/***************************************************************************
 *   Copyright (C) 2008-2013 by Heiko Koehn - KoehnHeiko@googlemail.com    *
 *   Copyright (C) 2014 by Ahmed Charles - acharles@outlook.com            *
 *   Copyright (C) 2014-2018, 2020, 2022-2024 by Stephen Lyons             *
 *                                               - slysven@virginmedia.com *
 *                                                                         *
 *   This program is free software; you can redistribute it and/or modify  *
 *   it under the terms of the GNU General Public License as published by  *
 *   the Free Software Foundation; either version 2 of the License, or     *
 *   (at your option) any later version.                                   *
 *                                                                         *
 *   This program is distributed in the hope that it will be useful,       *
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
 *   GNU General Public License for more details.                          *
 *                                                                         *
 *   You should have received a copy of the GNU General Public License     *
 *   along with this program; if not, write to the                         *
 *   Free Software Foundation, Inc.,                                       *
 *   59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.             *
 ***************************************************************************/


#include "TBuffer.h"

#include "mudlet.h"
#include "TEvent.h"
#include "TStringUtils.h"
#include "TTextProperties.h"
#include "widechar_width.h"
#include "TEncodingHelper.h"
#include "SentryWrapper.h"

#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonParseError>
#include <QJsonValue>
#include <QTextBoundaryFinder>
#include <QRegularExpression>

TChar::TChar(const QColor& foreground, const QColor& background, const TChar::AttributeFlags flags, const int linkIndex)
: mFgColor(foreground)
, mBgColor(background)
, mFlags(flags)
, mLinkIndex(linkIndex)
{
}

TChar::TChar(TConsole* pC)
: mFgColor(pC ? pC->mFormatCurrent.foreground() : QColorConstants::White)
, mBgColor(pC ? pC->mFormatCurrent.background() : QColorConstants::Black)
, mFlags(pC ? pC->mFormatCurrent.allDisplayAttributes() : AttributeFlag::None)
{
}

// Note: this operator compares ALL aspects of 'this' against 'other' which may
// not be wanted in every case:
bool TChar::operator==(const TChar& other)
{
    if (mIsSelected != other.mIsSelected) {
        return false;
    }
    if (mLinkIndex != other.mLinkIndex) {
        return false;
    }
    if (mFgColor != other.mFgColor) {
        return false;
    }
    if (mBgColor != other.mBgColor) {
        return false;
    }
    if (mFlags != other.mFlags) {
        return false;
    }
    return true;
}

// Copy constructor - because it is resetting the mIsSelected flag it is NOT a
// default copy constructor:
TChar::TChar(const TChar& copy)
: mFgColor(copy.mFgColor)
, mBgColor(copy.mBgColor)
, mFlags(copy.mFlags)
, mIsSelected(false)
, mLinkIndex(copy.mLinkIndex)
, mUnderlineColor(copy.mUnderlineColor)
, mOverlineColor(copy.mOverlineColor)
, mStrikeoutColor(copy.mStrikeoutColor)
, mHasCustomUnderlineColor(copy.mHasCustomUnderlineColor)
, mHasCustomOverlineColor(copy.mHasCustomOverlineColor)
, mHasCustomStrikeoutColor(copy.mHasCustomStrikeoutColor)
{
}

quint8 TChar::alternateFont() const
{
    // As this is the most likely case check it first:
    if (!(mFlags & AltFontMask)) {
        return 0;
    }

    if (mFlags & AltFont9) {
        return 9;
    }
    if (mFlags & AltFont8) {
        return 8;
    }
    if (mFlags & AltFont7) {
        return 7;
    }
    if (mFlags & AltFont6) {
        return 6;
    }
    if (mFlags & AltFont5) {
        return 5;
    }
    if (mFlags & AltFont4) {
        return 4;
    }
    if (mFlags & AltFont3) {
        return 3;
    }
    if (mFlags & AltFont2) {
        return 2;
    }
    return 1;
}

// Store for text and attributes (such as character color) to be drawn on screen
// Contents are rendered by a TTextEdit
TBuffer::TBuffer(Host* pH, TConsole* pConsole)
: mpConsole(pConsole)
, mBlack(pH->mBlack)
, mLightBlack(pH->mLightBlack)
, mRed(pH->mRed)
, mLightRed(pH->mLightRed)
, mLightGreen(pH->mLightGreen)
, mGreen(pH->mGreen)
, mLightBlue(pH->mLightBlue)
, mBlue(pH->mBlue)
, mLightYellow(pH->mLightYellow)
, mYellow(pH->mYellow)
, mLightCyan(pH->mLightCyan)
, mCyan(pH->mCyan)
, mLightMagenta(pH->mLightMagenta)
, mMagenta(pH->mMagenta)
, mLightWhite(pH->mLightWhite)
, mWhite(pH->mWhite)
, mForeGroundColor(pH->mFgColor)
, mForeGroundColorLight(pH->mFgColor)
, mBackGroundColor(pH->mBgColor)
, mpHost(pH)
, mTagWatchdog(std::make_unique<QTimer>())
{
    mTagWatchdog->setSingleShot(true);
    QObject::connect(mTagWatchdog.get(), &QTimer::timeout, [this]() { processMxpWatchdogCallback(); });
    // All additions to the buffer must use append()/appendLine() to preserve formatting via TChar.
    // Direct modification of the `buffer` vector may bypass formatting and should be avoided.
    clear();

#ifdef QT_DEBUG
    // Validate the encoding tables in case there has been an edit which breaks
    // things:
    for (const auto& table : csmEncodingTable.getEncodings()) {
        Q_ASSERT_X(table.size() == 128, "TBuffer", "Mis-sized encoding look-up table.");
    }
#endif
}

TBuffer::~TBuffer()
{
}

TBuffer::TBuffer(const TBuffer& other)
: bufferLine(other.bufferLine)
, buffer(other.buffer)
, lineBuffer(other.lineBuffer)
, timeBuffer(other.timeBuffer)
, promptBuffer(other.promptBuffer)
, mLinkStore(other.mLinkStore)
, mLinesLimit(other.mLinesLimit)
, mBatchDeleteSize(other.mBatchDeleteSize)
, mWrapAt(other.mWrapAt)
, mWrapIndent(other.mWrapIndent)
, mWrapHangingIndent(other.mWrapHangingIndent)
, mCursorY(other.mCursorY)
, mEchoingText(other.mEchoingText)
, mpConsole(other.mpConsole)
, mGotESC(other.mGotESC)
, mGotCSI(other.mGotCSI)
, mGotOSC(other.mGotOSC)
, mIsDefaultColor(other.mIsDefaultColor)
, mBlack(other.mBlack)
, mLightBlack(other.mLightBlack)
, mRed(other.mRed)
, mLightRed(other.mLightRed)
, mLightGreen(other.mLightGreen)
, mGreen(other.mGreen)
, mLightBlue(other.mLightBlue)
, mBlue(other.mBlue)
, mLightYellow(other.mLightYellow)
, mYellow(other.mYellow)
, mLightCyan(other.mLightCyan)
, mCyan(other.mCyan)
, mLightMagenta(other.mLightMagenta)
, mMagenta(other.mMagenta)
, mLightWhite(other.mLightWhite)
, mWhite(other.mWhite)
, mForeGroundColor(other.mForeGroundColor)
, mForeGroundColorLight(other.mForeGroundColorLight)
, mBackGroundColor(other.mBackGroundColor)
, mpHost(other.mpHost)
, mBold(other.mBold)
, mItalics(other.mItalics)
, mOverline(other.mOverline)
, mReverse(other.mReverse)
, mStrikeOut(other.mStrikeOut)
, mUnderline(other.mUnderline)
, mUnderlineWavy(other.mUnderlineWavy)
, mUnderlineDotted(other.mUnderlineDotted)
, mUnderlineDashed(other.mUnderlineDashed)
, mBlink(other.mBlink)
, mFastBlink(other.mFastBlink)
, mConcealed(other.mConcealed)
, mAltFont(other.mAltFont)
, mMudLine(other.mMudLine)
, mMudBuffer(other.mMudBuffer)
, mIncompleteSequenceBytes(other.mIncompleteSequenceBytes)
, lastLoggedFromLine(other.lastLoggedFromLine)
, lastloggedToLine(other.lastloggedToLine)
, lastTextToLog(other.lastTextToLog)
, mEncoding(other.mEncoding)
, mCurrentHyperlinkCommand(other.mCurrentHyperlinkCommand)
, mCurrentHyperlinkHint(other.mCurrentHyperlinkHint)
, mCurrentHyperlinkLinkId(other.mCurrentHyperlinkLinkId)
, mHyperlinkActive(other.mHyperlinkActive)
, mWatchdogPhase(other.mWatchdogPhase)
, mTagWatchdog(std::make_unique<QTimer>())
, mWatchdogTagSnapshot(other.mWatchdogTagSnapshot)
, mCurrentHyperlinkStyling(other.mCurrentHyperlinkStyling)
, mCurrentHyperlinkMenu(other.mCurrentHyperlinkMenu)
, mLinkStates(other.mLinkStates)
, mVisitedLinks(other.mVisitedLinks)
, mLinkOriginalBackgrounds(other.mLinkOriginalBackgrounds)
, mLinkOriginalCharacters(other.mLinkOriginalCharacters)
, mCurrentHoveredLinkIndex(other.mCurrentHoveredLinkIndex)
, mCurrentActiveLinkIndex(other.mCurrentActiveLinkIndex)
, mCurrentFocusedLinkIndex(other.mCurrentFocusedLinkIndex)
{
    mTagWatchdog->setSingleShot(true);
    QObject::connect(mTagWatchdog.get(), &QTimer::timeout, [this]() { processMxpWatchdogCallback(); });
}

TBuffer& TBuffer::operator=(const TBuffer& other)
{
    if (this != &other) {
        bufferLine = other.bufferLine;
        buffer = other.buffer;
        lineBuffer = other.lineBuffer;
        timeBuffer = other.timeBuffer;
        promptBuffer = other.promptBuffer;
        mLinkStore = other.mLinkStore;
        mLinesLimit = other.mLinesLimit;
        mBatchDeleteSize = other.mBatchDeleteSize;
        mWrapAt = other.mWrapAt;
        mWrapIndent = other.mWrapIndent;
        mWrapHangingIndent = other.mWrapHangingIndent;
        mCursorY = other.mCursorY;
        mEchoingText = other.mEchoingText;
        mpConsole = other.mpConsole;
        mGotESC = other.mGotESC;
        mGotCSI = other.mGotCSI;
        mGotOSC = other.mGotOSC;
        mIsDefaultColor = other.mIsDefaultColor;
        mBlack = other.mBlack;
        mLightBlack = other.mLightBlack;
        mRed = other.mRed;
        mLightRed = other.mLightRed;
        mLightGreen = other.mLightGreen;
        mGreen = other.mGreen;
        mLightBlue = other.mLightBlue;
        mBlue = other.mBlue;
        mLightYellow = other.mLightYellow;
        mYellow = other.mYellow;
        mLightCyan = other.mLightCyan;
        mCyan = other.mCyan;
        mLightMagenta = other.mLightMagenta;
        mMagenta = other.mMagenta;
        mLightWhite = other.mLightWhite;
        mWhite = other.mWhite;
        mForeGroundColor = other.mForeGroundColor;
        mForeGroundColorLight = other.mForeGroundColorLight;
        mBackGroundColor = other.mBackGroundColor;
        mpHost = other.mpHost;
        mBold = other.mBold;
        mItalics = other.mItalics;
        mOverline = other.mOverline;
        mReverse = other.mReverse;
        mStrikeOut = other.mStrikeOut;
        mUnderline = other.mUnderline;
        mUnderlineWavy = other.mUnderlineWavy;
        mUnderlineDotted = other.mUnderlineDotted;
        mUnderlineDashed = other.mUnderlineDashed;
        mBlink = other.mBlink;
        mFastBlink = other.mFastBlink;
        mConcealed = other.mConcealed;
        mAltFont = other.mAltFont;
        mMudLine = other.mMudLine;
        mMudBuffer = other.mMudBuffer;
        mIncompleteSequenceBytes = other.mIncompleteSequenceBytes;
        lastLoggedFromLine = other.lastLoggedFromLine;
        lastloggedToLine = other.lastloggedToLine;
        lastTextToLog = other.lastTextToLog;
        mEncoding = other.mEncoding;
        mCurrentHyperlinkCommand = other.mCurrentHyperlinkCommand;
        mCurrentHyperlinkHint = other.mCurrentHyperlinkHint;
        mCurrentHyperlinkLinkId = other.mCurrentHyperlinkLinkId;
        mHyperlinkActive = other.mHyperlinkActive;
        mWatchdogPhase = other.mWatchdogPhase;
        mWatchdogTagSnapshot = other.mWatchdogTagSnapshot;
        mCurrentHyperlinkStyling = other.mCurrentHyperlinkStyling;
        mCurrentHyperlinkMenu = other.mCurrentHyperlinkMenu;
        mLinkStates = other.mLinkStates;
        mVisitedLinks = other.mVisitedLinks;
        mLinkOriginalBackgrounds = other.mLinkOriginalBackgrounds;
        mLinkOriginalCharacters = other.mLinkOriginalCharacters;
        mCurrentHoveredLinkIndex = other.mCurrentHoveredLinkIndex;
        mCurrentActiveLinkIndex = other.mCurrentActiveLinkIndex;
        mCurrentFocusedLinkIndex = other.mCurrentFocusedLinkIndex;
        
        mTagWatchdog = std::make_unique<QTimer>();
        mTagWatchdog->setSingleShot(true);
        QObject::connect(mTagWatchdog.get(), &QTimer::timeout, [this]() { processMxpWatchdogCallback(); });
    }
    return *this;
}

// user-defined literal to represent megabytes
auto operator""_MB(unsigned long long const x)
        -> long
{ return 1024L*1024L*x; }

void TBuffer::setBufferSize(int requestedLinesLimit, int batch)
{
    if (requestedLinesLimit < 100) {
        requestedLinesLimit = 100;
    }
    if (batch >= requestedLinesLimit) {
        batch = requestedLinesLimit / 10;
    }
    // clip the maximum to something reasonable that the machine can handle
    auto max = getMaxBufferSize();
    if (requestedLinesLimit > max) {
        qWarning().nospace() << "setBufferSize(): " << requestedLinesLimit <<
                "lines for buffer requested but your computer can only handle " << max << ", clipping it";
        mLinesLimit = max;
    } else {
        mLinesLimit = requestedLinesLimit;
    }

    mBatchDeleteSize = batch;
}

// naive calculation to get a reasonable limit for a maximum buffer size
int TBuffer::getMaxBufferSize()
{
    const int64_t physicalMemoryTotal = mudlet::self()->getPhysicalMemoryTotal();
    // Mudlet is 32bit mainly on Windows, see where the practical limit for a process 2GB:
    // https://docs.microsoft.com/en-us/windows/win32/memory/memory-limits-for-windows-releases#memory-and-address-space-limits
    // 64bit: set to 80% of what is available to us, swap not included
    const int64_t maxProcessMemoryBytes = (QSysInfo::WordSize == 32) ? 1600_MB : (physicalMemoryTotal * 0.80);
    auto maxLines = (maxProcessMemoryBytes / TCHAR_IN_BYTES) / mpHost->mWrapAt;
    // now we've calculated how many lines can we fit in 80% of memory, ignoring memory use for other things like triggers/aliases, Lua scripts, etc
    // so shave that down by 20%
    maxLines = (maxLines / 100) * 80;

    return maxLines;
}

void TBuffer::updateColors()
{
    Host* pH = mpHost;
    if (!pH) {
        qWarning() << "TBuffer::updateColors() ERROR - Called when mpHost pointer is nullptr";
        return;
    }

    mBlack = pH->mBlack;
    mLightBlack = pH->mLightBlack;
    mRed = pH->mRed;
    mLightRed = pH->mLightRed;
    mLightGreen = pH->mLightGreen;
    mGreen = pH->mGreen;
    mLightBlue = pH->mLightBlue;
    mBlue = pH->mBlue;
    mLightYellow = pH->mLightYellow;
    mYellow = pH->mYellow;
    mLightCyan = pH->mLightCyan;
    mCyan = pH->mCyan;
    mLightMagenta = pH->mLightMagenta;
    mMagenta = pH->mMagenta;
    mLightWhite = pH->mLightWhite;
    mWhite = pH->mWhite;
    mForeGroundColor = pH->mFgColor;
    mForeGroundColorLight = pH->mFgColor;
    mBackGroundColor = pH->mBgColor;
}

QPoint TBuffer::getEndPos()
{
    int x = 0;
    int y = 0;
    if (!buffer.empty()) {
        y = buffer.size() - 1;
        if (!buffer.at(y).empty()) {
            x = buffer.at(y).size() - 1;
        }
    }
    QPoint P_end(x, y);
    return P_end;
}

// If buffer is empty zero is now returned and that is also returned if it only
// contains ONE line
int TBuffer::getLastLineNumber()
{
    if (static_cast<int>(buffer.size()) > 0) {
        return static_cast<int>(buffer.size()) - 1;
    } else {
        return 0; //-1;
    }
}

void TBuffer::addLink(bool trigMode, const QString& text, QStringList& command, QStringList& hint, TChar format, QVector<int> luaReference)
{
    const int id = mLinkStore.addLinks(command, hint, mpHost, luaReference);

    if (!trigMode) {
        append(text, 0, text.length(), format.mFgColor, format.mBgColor, format.mFlags, id);
    } else {
        appendLine(text, 0, text.length(), format.mFgColor, format.mBgColor, format.mFlags, id);
    }
}

/* ANSI color codes: sequence = "ESCAPE + [ code_1; ... ; code_n m"
      -----------------------------------------
      0 reset
      1 intensity bold on
      2 intensity faint on
      3 italics on
      4 underline on
      5 blink on slow
      6 blink on fast
      7 inverse on
      9 strikethrough on
      10 ? TODO
      22 intensity normal (not bold, not faint)
      23 italics off
      24 underline off
      25 blink off
      26 RESERVED (for proportional spacing)
      27 inverse off
      29 strikethrough off
      30 fg black
      31 fg red
      32 fg green
      33 fg yellow
      34 fg blue
      35 fg magenta
      36 fg cyan
      37 fg white
      39 fg default
      40 bg black
      41 bg red
      42 bg green
      43 bg yellow
      44 bg blue
      45 bg magenta
      46 bg cyan
      47 bg white
      49 bg default
      50 RESERVED (for proportional spacing)
      51 framed on
      52 encircled on
      53 overlined on
      54 framed / encircled off
      55 overlined off

      Notes for code 38/48:
      38:0 implementation defined (48:0 is NOT allowed)

      38:1 transparent foreground
      48:1 transparent background

      sequences for 24(32 for '4')-bit Color support:
      38:2:???:0-255:0-255:0-255:XXX:0-255:0-1 (direct) RGB space foreground color
      48:2:???:0-255:0-255:0-255:XXX:0-255:0-1 (direct) RGB space background color
      38:3:???:0-255:0-255:0-255:XXX:0-255:0-1 (direct) CMY space foreground color
      48:3:???:0-255:0-255:0-255:XXX:0-255:0-1 (direct) CMY space background color
      38:4:???:0-255:0-255:0-255:0-255:0-255:0-1 (direct) CMYK space foreground color
      48:4:???:0-255:0-255:0-255:0-255:0-255:0-1 (direct) CMYK space background color
      The third parameter is the color space id but this is expected to be the
      "default" value which is an empty string. The seventh parameter may be used
      to specify a tolerance value (an integer) and parameter eight may be used
      to specify a colour space associated with the tolerance (0 for CIELUV,
      1 for CIELAB).

      sequences for (indexed) 256 Color support:
      38:5:0-256 (indexed) foreground color
      48:5:0-256 (indexed) background color:
          0x00-0x07:   0 -   7 standard colors (as in ESC [ 30–37 m)
          0x08-0x0F:   8 -  15 high intensity colors (as in ESC [ 90–97 m)
          0x10-0xE7:  16 - 231 6 × 6 × 6 = 216 colors: 16 + 36 × r + 6 × g + b (0 ≤ r, g, b ≤ 5)
          0xE8-0xFF: 232 - 255 grayscale from black to white in 24 steps

      Also note that for the 38 and 48 codes the parameter elements SHOULD be
      separated by ':' but some interpretations erroneously use ';'.  Also
      "empty" parameter elements represent a default value and that empty
      elements at the end can be omitted.
 */

void TBuffer::translateToPlainText(std::string& incoming, const bool isFromServer)
{
    // What can appear in a CSI Parameter String (Ps) byte or at least for it
    // to be something we can handle:
    const QByteArray cParameter = QByteArrayLiteral("0123456789;:");
    // What can appear in the initial position of a CSI Parameter String (Ps) byte:
    const QByteArray cParameterInitial = QByteArrayLiteral("0123456789;:<=>?");
    // What can appear in a CSI Intermediate byte (includes a quote character in
    // the middle of the text here which has to be escaped with a backslash):
    const QByteArray cIntermediate = QByteArrayLiteral(" !\"#$%&'()*+,-./");
    // What can appear in a CSI final byte position - (includes a backslash
    // which has to be doubled to include it in here):
    const QByteArray cFinal = QByteArrayLiteral("@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~");

    // As well as enabling the prepending of left-over bytes from last packet
    // from the MUD server this may help in high frequency interactions to
    // protect this process from the supplied string being modified
    // asynchronously by the QNetwork code that runs in another thread:
    std::string localBuffer;

    Host* pHost = mpHost;
    if (!pHost) {
        qWarning() << "TBuffer::translateToPlainText(...) ERROR: Cannot access Host instance at this time - data has been lost.";
        return; // We really have a problem
    }

    // Check this each packet
    const QByteArray usedEncoding = mpHost->mTelnet.getEncoding();
    if (mEncoding != usedEncoding) {
        encodingChanged(usedEncoding);
        // Will have to dump any stored bytes as they will be in the old
        // encoding and the following code block to prepend them is used for
        // both bytes that are held over as part of a multi-byte encoding that
        // was incomplete at the end of the last packet AND ALSO for ANSI code
        // sequences that were not complete at the end of the last packet:
        if (!mIncompleteSequenceBytes.empty()) {
#if defined(DEBUG_SGR_PROCESSING) || defined(DEBUG_OSC_PROCESSING) || defined(DEBUG_UTF8_PROCESSING) || defined(DEBUG_GB_PROCESSING) || defined(DEBUG_BIG5_PROCESSING)
            qDebug() << "TBuffer::translateToPlainText(...) WARNING - Dumping residual bytes that were carried over from previous packet onto incoming data - the encoding has changed and they may no longer be usable!";
#endif
            mIncompleteSequenceBytes.clear();;
        }
    }

    if (isFromServer && !mIncompleteSequenceBytes.empty()) {
#if defined(DEBUG_SGR_PROCESSING) || defined(DEBUG_OSC_PROCESSING) || defined(DEBUG_UTF8_PROCESSING) || defined(DEBUG_GB_PROCESSING) || defined(DEBUG_BIG5_PROCESSING)
        qDebug() << "TBuffer::translateToPlainText(...) Prepending residual bytes onto incoming data!";
#endif
        localBuffer = mIncompleteSequenceBytes + incoming;
        mIncompleteSequenceBytes.clear();
    } else {
        localBuffer = incoming;
    }

    crashIfRequested();

    // Fixup table for our own, substitute QTextCodecs:
    QByteArray encodingTableToUse{mEncoding};
    if (mEncoding == "M_CP437") {
        encodingTableToUse = "CP437";
    } else if (mEncoding == "M_CP667") {
        encodingTableToUse = "CP667";
    } else if (mEncoding == "M_CP737") {
        encodingTableToUse = "CP737";
    } else if (mEncoding == "M_CP869") {
        encodingTableToUse = "CP869";
    } else if (mEncoding == "M_MEDIEVIA") {
        encodingTableToUse = "MEDIEVIA";
    }

    const QVector<QChar> encodingLookupTable = csmEncodingTable.getLookupTable(encodingTableToUse);
    // If the encoding is "ASCII", "ISO 8859-1", "UTF-8", "GBK", "GB18030",
    // "BIG5", "BIG5-HKSCS" or "EUC-KR" (which are not in the table)
    // encodingLookupTable will be empty otherwise the 128 values in the
    // returned table will be used for all the text data that gets through the
    // following ANSI code and other out-of-band data processing - doing this
    // means that a (fast) lookup in the QVector can be done as opposed to a
    // repeated switch(...) and branch to one of a series of decoding methods
    // each with another up to 128 value switch()

    size_t localBufferLength = localBuffer.length();
    size_t localBufferPosition = 0;
    if (!localBufferLength) {
        return;
    }

    // If we are resolving/interpolating an MXP entity, the interpolated text
    // ends at localBuffer[endOfMXPEntity - 1]. This variable used to avoid an
    // (infinite) recursion like <!EN E "foobar&E;>&E;
    // Recursively interpolating a predefined entity like <!EN E "foobar&frac12;>&E;
    // will work though.
    size_t endOfMXPEntity = 0;

    // A similar index which points behind the name of a literal entity name like
    // &unknown; which does not exist and will be printed literal, w/o
    // any MXP interpretation. Again, this avoid endless recursion trying to
    // resolve an unsolvable entity. We need the hassle in both cases, as the
    // the resolved values may be in a character encoding that must be decoded by
    // Mudlet.
    size_t endOfLiteralEntity = 0;

    while (true) {
        if (localBufferPosition >= localBufferLength) {
            return;
        }

        char& ch = localBuffer[localBufferPosition];
        if (ch == '\033') {
            if (!mGotOSC) {
                // The terminator for an OSC is the String Terminator but that
                // is the ESC character followed by (the single character)
                // '\\' so must not respond to an ESC here - though the code
                // arrangement should avoid looping around this loop while
                // seeking this character pair anyhow...
                mGotESC = true;
                ++localBufferPosition;
                continue;
            }
        }

        if (mGotESC && (ch == '[' || ch == ']')) {
            mGotESC = false;
            mGotCSI = (ch == '[');
            mGotOSC = (ch == ']');
            ++localBufferPosition;
            continue;
        }

        if (mGotCSI) {
            // Lookahead and try and see what we are processing
            // At the start of a CSI sequence the only valid character is one of:
            // "0-9:;<=>?" if it is one of "0-9:;" then it is a
            // "parameter-string" ELSE if it is one of '<', '=', '>' or '?' it
            // IS a private/experimental and not covered by the ECMA-48
            // specifications..
            // After the first character the remaining characters of the
            // parameter string will be in the range "0-9:;" only
            size_t const spanStart = localBufferPosition;
            size_t spanEnd = spanStart;
            while (spanEnd < localBufferLength
                   && ((((spanStart < spanEnd) && cParameterInitial.indexOf(localBuffer[spanEnd]) >= 0))
                      ||((spanStart == spanEnd) && cParameter.indexOf(localBuffer[spanEnd]) >= 0))) {
                ++spanEnd;
            }

            // Test whether the first byte is within the usable subset of the
            // allowed value - or not:
            if (cParameter.indexOf(localBuffer[spanStart]) == -1 && cParameterInitial.indexOf(localBuffer[spanStart]) >= 0) {
                // Oh dear, the CSI parameter string sequence begins with one of
                // the reserved characters ('<', '=', '>' or '?') which we
                // can/do not handle

                qDebug().noquote().nospace() << "TBuffer::translateToPlainText(...) INFO - detected a private/reserved CSI sequence beginning with \"CSI" << localBuffer.substr(spanStart, spanEnd - spanStart).c_str() << "\" which Mudlet cannot interpret.";
                // So skip over it as far as we can - will still possibly have
                // garbage beyond the end which will still be shown...
                localBufferPosition += 1 + spanEnd - spanStart;
                mGotCSI = false;
                // Go around while loop again:
                continue;
            }

            if (spanEnd >= localBufferLength || cParameter.indexOf(localBuffer[spanEnd]) >= 0) {
                // We have gone to the end of the buffer OR the last character
                // in the buffer is still within a CSI sequence - therefore we
                // have got a split between data packets and are not in a
                // position to process the current line further...

                mIncompleteSequenceBytes = localBuffer.substr(spanStart);
                return;
            }

            // Now we can take a peek at what the next character is, it could
            // be an optional (and we are not expecting this) "intermediate
            // byte" which is space or one of "!"#$%&'()*+,-./" or a "final
            // byte" which is what determines what on earth the CSI is for, it
            // should be in the (ASCII) range '@' to '~' and the end of that
            // range 'p' to '~' is for "private" or "experimental" use.

            if (cIntermediate.indexOf(localBuffer[spanEnd]) >= 0) {
                // We do not handle any sequences with intermediate bytes
                // Report it and then ignore it, try and find out what the byte
                // afterwards is as it might help to debug things
                if (spanEnd + 1 < localBufferLength) {
                    // Yeah there is another byte we can report as the final byte
                    qDebug().noquote().nospace() << "TBuffer::translateToPlainText(...) INFO - detected a CSI sequence with an 'intermediate' byte ('" << localBuffer[spanEnd] << "') and a 'final' byte ('" << localBuffer[spanEnd+1] << "') which Mudlet cannot interpret.";
                } else {
                    qDebug().noquote().nospace() << "TBuffer::translateToPlainText(...) INFO - detected a CSI sequence with an 'intermediate' byte ('" << localBuffer[spanEnd] << "') which Mudlet cannot interpret.";
                }
                // So skip over it as far as we can - will still be possible to
                // have garbage beyond the end which will still be shown...
                localBufferPosition += 1 + spanEnd - spanStart;
                mGotCSI = false;
                // Go around while loop again:
                continue;
            }

            if (cFinal.indexOf(localBuffer[spanEnd]) >= 0) {
                // We have a valid CSI sequence - but is it one we handle?
                // We currently only handle the 'm' for SGR and the 'z' for
                // Zuggsoft's MXP protocol:
                const quint8 modeChar = static_cast<unsigned char>(localBuffer[spanEnd]);
                switch (modeChar) {
                case static_cast<quint8>('m'):
                    // We have a complete SGR sequence:
#if defined(DEBUG_SGR_PROCESSING)
                    qDebug().nospace().noquote() << "    Consider the SGR sequence: \"" << localBuffer.substr(localBufferPosition, spanEnd - spanStart).c_str() << "\"";
#endif
                    decodeSGR(QString(localBuffer.substr(localBufferPosition, spanEnd - spanStart).c_str()));
                    break;

                case static_cast<quint8>('z'):
                    // We have a control sequence for MXP
#if defined(DEBUG_MXP_PROCESSING)
                    qDebug().nospace().noquote() << "    Consider the MXP control sequence: \"" << localBuffer.substr(localBufferPosition, spanEnd - spanStart).c_str() << "\"";
#endif
                    if (isFromServer && (mpHost->mTelnet.isMXPEnabled() || mpHost->getForceMXPProcessorOn())) {
                        mGotCSI = false;

                        const QString code = QString(localBuffer.substr(localBufferPosition, spanEnd - spanStart).c_str());
                        mpHost->mMxpProcessor.setMode(code);
                    }

                    break;

                case static_cast<quint8>('C'): {
                    // A workaround for the ONE cursor movement command we CAN
                    // emulate - the CUF Cursor forward one:
                    // Needed for mud.durismud.com see forum message topic:
                    // https://forums.mudlet.org/viewtopic.php?f=9&t=22887
                    const int dataLength = spanEnd - spanStart;
                    const QByteArray temp = QByteArray::fromRawData(localBuffer.substr(localBufferPosition, dataLength).c_str(), dataLength);
                    bool isOk = false;
                    const int spacesNeeded = temp.toInt(&isOk);
                    if (isOk && spacesNeeded > 0) {
                        // Note: we are using the background color for the
                        // foreground color as well so that we are transparent:
                        const TChar c(mBackGroundColor, mBackGroundColor, computeCurrentAttributeFlags());
                        for (int spaceCount = 0; spaceCount < spacesNeeded; ++spaceCount) {
                            mMudLine.append(QChar::Space);
                            mMudBuffer.push_back(c);
                        }
                        // For debugging:
//                        qDebug().noquote().nospace() << "TBuffer::translateToPlainText(...) INFO - CUF (cursor forward) sequence of form CSI" << temp << "C received, converting into " << spacesNeeded << " spaces.";
                    } else {
                        qDebug().noquote().nospace() << "TBuffer::translateToPlainText(...) INFO - Unhandled sequence of form CSI..." << temp << "C received, that is supposed to be a CUF (cursor forward) sequence but doesn't make sense, Mudlet will ignore it.";
                    }

                }
                    break;

                case static_cast<quint8>('J'): {
                    /*
                     * Also seen in output from mud.durismud.com see 'C' case above:
                     * Is ED 'Erase Display' command and has three variants:
                     * * 0 (or omitted): clear from cursor to end of screen
                     *   - which is a NOP for us!
                     * * 1: clear from cursor to beginning of screen
                     *   - which is a NWIH for us!
                     * * 2: clear entire screen and delete all lines saved in
                     *   scrollback buffer - which is again a NWIH for us...!
                     */
                    const int dataLength = spanEnd - spanStart;
                    const QByteArray temp = QByteArray::fromRawData(localBuffer.substr(localBufferPosition, dataLength).c_str(), dataLength);
                    bool isOk = false;
                    const int argValue = temp.toInt(&isOk);
                    if (isOk) {
                        if (argValue >= 0 && argValue < 3) {
                            qDebug().noquote().nospace() << "TBuffer::translateToPlainText(...) INFO - ED (erase in display) sequence of form CSI" << temp << "J received,\nrejecting as incompatible with Mudlet.";
                        } else {
                            qDebug().noquote().nospace() << "TBuffer::translateToPlainText(...) INFO - Invalid ED (erase in display) sequence of form CSI" << temp << "J received,\nwhich Mudlet will ignore.";
                        }
                    } else {
                        qDebug().noquote().nospace() << "TBuffer::translateToPlainText(...) INFO - Unhandled sequence of form CSI..." << temp << "J received, that is supposed to\nbe a ED (erase in display) sequence but it doesn't make sense, Mudlet will ignore it.";
                    }
                }
                    break;

                default: // Unhandled other (valid) CSI final byte sequences will end up here
                    qDebug().noquote().nospace() << "TBuffer::translateToPlainText(...) INFO - Unhandled sequence of form CSI..." << localBuffer[spanEnd] << " received, Mudlet will ignore it.";

                } // End of switch(modeChar) {}
            } else {
                qDebug().noquote().nospace() << "TBuffer::translateToPlainText(...) INFO - detected an invalid CSI sequence beginning with \"CSI" << localBuffer.substr(spanStart, spanEnd - spanStart).c_str() << " which Mudlet will ignore.";
            }  // End of (isAValidFinalByte) {}

            mGotCSI = false;
            localBufferPosition += 1 + spanEnd - spanStart;
            // Go around while loop again:
            continue;

        } // End of if (mGotCSI)

        if (mGotOSC) {
            // Lookahead and find end of sequence (the ST string terminator)
            // DANGER, WILL ROBINSON! Should an OSC be received without a
            // terminator then all data will just be swallowed into the buffer

            // Valid characters inside an OSC are: a "command string" or a
            // "character string".
            // A "command string" is a sequence of bit combinations in the range
            // <BS><TAB><LF><VT><FF><CR> and ASCII printables from Space to '~'
            // A "character string" is a sequence of any character except Start
            // of String (SOS) or String Terminator (ST) and the latter is ESC
            // followed by '\\' (a single \ BTW) in the 7-bit code case (the
            // former is encoded as ESC followed by 'X'):
            size_t const spanStart = localBufferPosition;
            size_t spanEnd = spanStart;
            // It is safe to look at spanEnd-1 even at the starting position
            // because we already know that the localBuffer extends backwards
            // that far (it will be the ']' character!)
            while (spanEnd < localBufferLength
                   && (spanEnd == 0 || localBuffer[spanEnd-1] != '\033')
                   && (localBuffer[spanEnd] != '\\')) {
                ++spanEnd;
            }

            if (localBuffer[spanEnd] != '\\') {
                // The last character in the buffer is NOT the expected ST
                // - therefore we have probably got a split between
                // data packets and are not in a position to process the
                // current line further...

                mIncompleteSequenceBytes = localBuffer.substr(spanStart);
                return;
            }

            decodeOSC(QString(localBuffer.substr(localBufferPosition, spanEnd - spanStart - 1).c_str()));
            mGotOSC = false;
            localBufferPosition += 1 + spanEnd - spanStart;
            // Go around while loop again:
            continue;
        }

        // We are outside of a CSI or OSC sequence if we get to here:

        if (localBufferPosition >= endOfLiteralEntity && mpHost->mMxpProcessor.isEnabled()) {
            if (mpHost->mTelnet.isMXPEnabled() || mpHost->getForceMXPProcessorOn()) {
                if (mpHost->mMxpProcessor.mode() != MXP_MODE_LOCKED) {
                    // The comparison signals to the processor, if custom entities may be resolved
                    // (countermeasure against infinite recursion)
                    TMxpProcessingResult const result =
                            mpHost->mMxpProcessor.processMxpInput(ch, localBufferPosition >= endOfMXPEntity);
                    if (!mTagWatchdog->isActive()
                    && mpHost->mMxpProcessor.getMxpTagBuilder().isInsideTag()
                    && !mpHost->mMxpProcessor.getMxpTagBuilder().getRawTagContent().empty()) {
                        mWatchdogPhase = WatchdogPhase::Phase1_Snapshot;
                        mTagWatchdog->start(MAX_TAG_TIMEOUT_MS);
                    }

                    switch (result) {
                    case HANDLER_NEXT_CHAR:
                        localBufferPosition++;
                        continue;
                    case HANDLER_COMMIT_LINE: // BR tag or &newline;
                        ch = '\n';
                        goto COMMIT_LINE;
                    case HANDLER_INSERT_ENTITY_CUST:
                        // custom entity value set with <!EN>, recurse except for other custom entities
                        [[fallthrough]];
                    case HANDLER_INSERT_ENTITY_LIT: {
                        // Unknown entity name like &unknown; push back into buffer for codeset interpretation,
                        // but no MXP parsing.

                        // We replace the already processed text with the entity value into the buffer and restart
                        // processing it for charset encoding but with limited MXP handling
                        size_t valueLength = mpHost->mMxpProcessor.getEntityValue().length();
                        localBuffer.replace(0, localBufferPosition + 1, mpHost->mMxpProcessor.getEntityValue().toLatin1());

                        if (result == HANDLER_INSERT_ENTITY_LIT) {
                            if (localBufferPosition < endOfMXPEntity) {
                                // This is a special case, our unknown entity might actually be a custom one
                                // inside a custom one which we refused to resolve to avoid an endless recursion.
                                // So we carefully adjust the end marker s.t. custom entities are not reenabled
                                // too early
                                endOfMXPEntity -= localBufferPosition + 1 - valueLength;
                                endOfLiteralEntity = valueLength;
                            } else {
                                endOfMXPEntity = valueLength;
                            }
                            endOfLiteralEntity = valueLength;
                        } else {
                            // HANDLER_INSERT_ENTITY_CUST
                            endOfMXPEntity = valueLength;
                            endOfLiteralEntity = 0;
                        }

                        // Now restart the loop to parse the newly inserted text
                        localBufferLength = localBuffer.length();
                        localBufferPosition = 0;
                        continue;
                    }
                    case HANDLER_INSERT_ENTITY_SYS: {
                        // System entities are literal QString / UTF values which we just 'print'
                        // There is no further MXP or Codeset evaluation

                        TChar::AttributeFlags attributeFlags = computeCurrentAttributeFlags();
                        attributeFlags &= ~(TChar::FastBlink | TChar::Concealed | TChar::AltFontMask);
                        TChar c((!mIsDefaultColor && mBold) ? mForeGroundColorLight : mForeGroundColor, mBackGroundColor, attributeFlags);

                        size_t valueLength = mpHost->mMxpProcessor.getEntityValue().length();
                        mMudLine.append(mpHost->mMxpProcessor.getEntityValue());
                        // We also need to set the color attributes for the special character
                        while (valueLength--) {
                            mMudBuffer.push_back(c);
                        }
                        // We already handled the input, go to the next character
                        localBufferPosition++;
                        continue;
                    }
                    default:
                        //HANDLER_FALL_THROUGH -> do nothing
                        assert(localBuffer[localBufferPosition] == ch);
                    }
                } else if (mpHost->mMxpProcessor.getMxpTagBuilder().isInsideTag()) {
                    localBufferPosition++;
                    continue;
                } else {
                    mpHost->mMxpProcessor.processRawInput(ch);
                }
            }

            if (CHAR_IS_COMMIT_CHAR(ch)) {
                // after a newline (but not a <br>) return to default mode
                mpHost->mMxpProcessor.resetToDefaultMode();
            }
        }

COMMIT_LINE:
        if (commitLine(ch, localBufferPosition)) {
            continue;
        }
        // PLACEMARKER: Incoming text decoding
        // Used to double up the TChars for Utf-8 byte sequences that produce
        // a surrogate pair (non-BMP):
        bool isTwoTCharsNeeded = false;

        if (!encodingLookupTable.isEmpty()) {
            auto index = static_cast<quint8>(ch);
            if (index < 128) {
                mMudLine.append(QChar::fromLatin1(ch));
            } else {
                mMudLine.append(encodingLookupTable.at(index - 128));
            }
        } else if (mEncoding == "ISO 8859-1") {
            mMudLine.append(QString(QChar::fromLatin1(ch)));
        } else if (mEncoding == "GBK") {
            if (!processGBSequence(localBuffer, isFromServer, false, localBufferLength, localBufferPosition, isTwoTCharsNeeded)) {
                // We have run out of bytes and we have stored the unprocessed
                // ones but we need to bail out NOW!
                return;
            }
        } else if (mEncoding == "GB18030") {
            if (!processGBSequence(localBuffer, isFromServer, true, localBufferLength, localBufferPosition, isTwoTCharsNeeded)) {
                // We have run out of bytes and we have stored the unprocessed
                // ones but we need to bail out NOW!
                return;
            }
        } else if (mEncoding == "EUC-KR") {
            if (!processEUC_KRSequence(localBuffer, isFromServer, localBufferLength, localBufferPosition, isTwoTCharsNeeded)) {
                // We have run out of bytes and we have stored the unprocessed
                // ones but we need to bail out NOW!
                return;
            }
        } else if (mEncoding == "BIG5" || mEncoding == "BIG5-HKSCS") {
            if (!processBig5Sequence(localBuffer, isFromServer, localBufferLength, localBufferPosition, isTwoTCharsNeeded)) {
                // We have run out of bytes and we have stored the unprocessed
                // ones but we need to bail out NOW!
                return;
            }
        } else if (mEncoding == "UTF-8") {
            if (!processUtf8Sequence(localBuffer, isFromServer, localBufferLength, localBufferPosition, isTwoTCharsNeeded)) {
                // We have run out of bytes and we have stored the unprocessed
                // ones but we need to bail out NOW!
                return;
            }
        } else {
            // Default - no encoding case - reject anything that has MS Bit set
            // as that isn't ASCII which is what no encoding specifies!
            if (ch & 0x80) {
                // Was going to ignore this byte, not add a TChar instance
                // either and move on:
                // ++localBufferPosition;
                // continue;
                // but instead insert the "Replacement Character Marker"
                mMudLine.append(QChar::ReplacementCharacter);
            } else {
                mMudLine.append(ch);
            }
        }

        TChar c((!mIsDefaultColor && mBold) ? mForeGroundColorLight : mForeGroundColor, mBackGroundColor, computeCurrentAttributeFlags());

        if (mHyperlinkActive) {
            c.mLinkIndex = mCurrentHyperlinkLinkId;

            // Store the original ANSI-formatted character before applying CSS styling
            // This is needed for ANSI base restoration when pseudo-classes are inactive
            if (!mLinkOriginalCharacters.contains(mCurrentHyperlinkLinkId)) {
                mLinkOriginalCharacters[mCurrentHyperlinkLinkId] = c;
#if defined(DEBUG_OSC_PROCESSING)
                qDebug().nospace().noquote() << "TBuffer::translateToPlainText(): Stored original character for link " << mCurrentHyperlinkLinkId
                                              << " with ANSI colors: fg=" << c.mFgColor.name()
                                              << " bg=" << c.mBgColor.name()
                                              << " flags=" << c.mFlags;
#endif
            }

            // Apply enhanced hyperlink styling
            if (mCurrentHyperlinkStyling.hasForegroundColor) {
                c.mFgColor = mCurrentHyperlinkStyling.foregroundColor;
            }
            if (mCurrentHyperlinkStyling.hasBackgroundColor) {
                c.mBgColor = mCurrentHyperlinkStyling.backgroundColor;
            }

            // Apply text decorations
            if (mCurrentHyperlinkStyling.isUnderlined) {
                c.mFlags |= TChar::Underline;

                // Apply specific underline style based on underlineStyle enum
                switch (mCurrentHyperlinkStyling.underlineStyle) {
                    case Mudlet::HyperlinkStyling::UnderlineWavy:
                        c.mFlags |= TChar::UnderlineWavy;
                        break;
                    case Mudlet::HyperlinkStyling::UnderlineDotted:
                        c.mFlags |= TChar::UnderlineDotted;
                        break;
                    case Mudlet::HyperlinkStyling::UnderlineDashed:
                        c.mFlags |= TChar::UnderlineDashed;
                        break;
                    case Mudlet::HyperlinkStyling::UnderlineSolid:
                    case Mudlet::HyperlinkStyling::UnderlineNone:
                    default:
                        // Standard solid underline (no additional flags needed)
                        break;
                }
            }
            if (mCurrentHyperlinkStyling.isOverlined) {
                c.mFlags |= TChar::Overline;
            }
            if (mCurrentHyperlinkStyling.isStrikeOut) {
                c.mFlags |= TChar::StrikeOut;
            }

            // Apply decoration colors - only for active decorations
            if (mCurrentHyperlinkStyling.hasUnderlineColor && mCurrentHyperlinkStyling.isUnderlined) {
                c.setUnderlineColor(mCurrentHyperlinkStyling.underlineColor);
            }
            if (mCurrentHyperlinkStyling.hasOverlineColor && mCurrentHyperlinkStyling.isOverlined) {
                c.setOverlineColor(mCurrentHyperlinkStyling.overlineColor);
            }
            if (mCurrentHyperlinkStyling.hasStrikeoutColor && mCurrentHyperlinkStyling.isStrikeOut) {
                c.setStrikeoutColor(mCurrentHyperlinkStyling.strikeoutColor);
            }

            if (mCurrentHyperlinkStyling.isBold) {
                c.mFlags |= TChar::Bold;
            }
            if (mCurrentHyperlinkStyling.isItalic) {
                c.mFlags |= TChar::Italic;
            }

            // Only apply underline if explicitly set in styling (respects OSC 8 default of no underline)
            // Note: This differs from other Mudlet hyperlinks which default to underlined
        }

        if (mpHost->mMxpClient.isInLinkMode()) {
            c.mLinkIndex = mLinkStore.getCurrentLinkID();
            c.mFlags |= TChar::Underline;
        }

        if (mpHost->mMxpClient.hasFgColor()) {
            c.mFgColor = mpHost->mMxpClient.getFgColor();
        }

        if (mpHost->mMxpClient.hasBgColor()) {
            c.mBgColor = mpHost->mMxpClient.getBgColor();
        }

        if (isTwoTCharsNeeded) {
            // CHECK: Do we need to duplicate stuff for mMXP_LINK_MODE - yes I think we do:
            mMudBuffer.push_back(c);
            mMudBuffer.push_back(c);
        } else {
            mMudBuffer.push_back(c);
        }

        ++localBufferPosition;
    }
}


bool TBuffer::commitLine(char ch, size_t& localBufferPosition)
{
    if (CHAR_IS_COMMIT_CHAR(ch)) {
        // DE: MUD Zeilen werden immer am Zeilenanfang geschrieben
        // EN: MUD lines are always written at the beginning of the line

        // FIXME: This is the point where we should renormalise the new text
        // data - of course there is the theoretical chance that the new
        // text would alter the prior contents but as that is on a separate
        // line there should not be any changes to text before a line feed
        // which sort of seems to be implied by the current value of ch:

        // Qt struggles to report blank lines on Windows to screen readers, this is a workaround
        // https://bugreports.qt.io/browse/QTBUG-105035
        if (Q_UNLIKELY(mMudLine.isEmpty())) {
            if (mpHost->mBlankLineBehaviour == Host::BlankLineBehaviour::Hide) {
                localBufferPosition++;
                return true;
            } else if (mpHost->mBlankLineBehaviour == Host::BlankLineBehaviour::ReplaceWithSpace) {
                // Note: we are using the background color for the
                // foreground color as well so that we are transparent:
                const TChar c(mBackGroundColor, mBackGroundColor, computeCurrentAttributeFlags());
                mMudLine.append(QChar::Space);
                mMudBuffer.push_back(c);
            }
        }

        if (static_cast<size_t>(mMudLine.size()) != mMudBuffer.size()) {
            qWarning() << "TBuffer::translateToPlainText(...) WARNING: mismatch in new text "
                        "data character and attribute data items!";
        }
        if (!lineBuffer.back().isEmpty()) {
            if (!mMudLine.isEmpty()) {
                lineBuffer << mMudLine;
            } else {
                if (ch == '\r') {
                    ++localBufferPosition;
                    return true; //empty timer posting
                }
                lineBuffer << QString();
            }
            buffer.push_back(mMudBuffer);
            timeBuffer << QTime::currentTime().toString(mudlet::smTimeStampFormat);
            if (ch == '\xff') {
                promptBuffer.append(true);
            } else {
                promptBuffer.append(false);
            }
        } else {
            if (!mMudLine.isEmpty()) {
                lineBuffer.back().append(mMudLine);
            } else {
                if (ch == '\r') {
                    ++localBufferPosition;
                    return true; //empty timer posting
                }
                lineBuffer.back().append(QString());
            }
            buffer.back() = mMudBuffer;
            timeBuffer.back() = QTime::currentTime().toString(mudlet::smTimeStampFormat);
            if (ch == '\xff') {
                promptBuffer.back() = true;
            } else {
                promptBuffer.back() = false;
            }
        }
        mMudLine.clear();
        mMudBuffer.clear();
        const int line = lineBuffer.size() - 1;
        mpHost->mpConsole->runTriggers(line);

        // Only use of TBuffer::wrap(), breaks up new text
        // NOTE: it MAY have been clobbered by the trigger engine!
        const int addedLines = wrapLine(line, mWrapAt, mWrapIndent, mWrapHangingIndent);

        // Start a new, but empty line in the various buffers
        log(lineBuffer.size() - 1, lineBuffer.size() - 1);

        ++localBufferPosition;
        // Suppress new empty line IFF echoes already created a new empty line
        // i.e. add newline if no added lines or the lastline isn't empty
        if (addedLines == 0 || !lineBuffer.back().isEmpty()) {
            std::deque<TChar> const newLine;
            buffer.push_back(newLine);
            lineBuffer.push_back(QString());
            timeBuffer.push_back(QString());
            promptBuffer << false;
        }

        if (static_cast<int>(buffer.size()) > mLinesLimit) {
            // Whilst we also include a call to TConsole::handleLinesOverflowEvent(...)
            // in all other methods where the following is used (because
            // both need to monitor the number of lines of text in the
            // buffer) the event that the former may be required to
            // generate is NOT used for the TMainConsole case whereas this
            // (translateToPlainText(...)) method is ONLY for that one:
            shrinkBuffer();
        }
        return true;
    }
    return false;
}

void TBuffer::processMxpWatchdogCallback()
{
    TMxpNodeBuilder&    tagBuilder = mpHost->mMxpProcessor.getMxpTagBuilder();
    std::string         currentTagContent = tagBuilder.getRawTagContent();
    bool                isMxpParserFrozen = !currentTagContent.empty()
                                            && currentTagContent.starts_with(mWatchdogTagSnapshot)
                                            && tagBuilder.isInsideTag();

    if (mWatchdogPhase == WatchdogPhase::Phase1_Snapshot) {
        mWatchdogTagSnapshot = currentTagContent;
        mWatchdogPhase = WatchdogPhase::Phase2_Unfreeze;
        mTagWatchdog->start(MAX_TAG_TIMEOUT_MS);
    } else if (mWatchdogPhase == WatchdogPhase::Phase2_Unfreeze) {
        if (isMxpParserFrozen) {
            mpHost->mMxpProcessor.setLastEntityValue(QString::fromStdString('<' + currentTagContent));
            QTimer::singleShot(0, [this] () {
                const TChar style(mForeGroundColor, mBackGroundColor, computeCurrentAttributeFlags());
                QString     lastEntityValue = mpHost->mMxpProcessor.getEntityValue();
                size_t      unusedBufferPosition = 0;

                mMudLine.append(lastEntityValue);
                for (qsizetype i = 0; i < lastEntityValue.size(); ++i) {
                    mMudBuffer.push_back(style);
                }
                commitLine('\r', unusedBufferPosition);
                mpHost->mMxpProcessor.getMxpTagBuilder().reset();
                mpHost->mpConsole->finalize();
            });
        }
        mWatchdogPhase = WatchdogPhase::None;
    }
}

TChar::AttributeFlags TBuffer::computeCurrentAttributeFlags() const {
    return ((mIsDefaultColor ? mBold || mpHost->mMxpClient.bold() : false) ? TChar::Bold : TChar::None)
            | (mItalics || mpHost->mMxpClient.italic() ? TChar::Italic : TChar::None)
            | (mOverline ? TChar::Overline : TChar::None)
            | (mReverse ? TChar::Reverse : TChar::None)
            | (mStrikeOut || mpHost->mMxpClient.strikeOut() ? TChar::StrikeOut : TChar::None)
            | (mUnderline || mpHost->mMxpClient.underline() ? TChar::Underline : TChar::None)
            | (mUnderlineWavy ? TChar::UnderlineWavy : TChar::None)
            | (mUnderlineDotted ? TChar::UnderlineDotted : TChar::None)
            | (mUnderlineDashed ? TChar::UnderlineDashed : TChar::None)
            | (mFastBlink ? TChar::FastBlink : (mBlink ? TChar::Blink :TChar::None))
            | (TChar::alternateFontFlag(mAltFont))
            | (mConcealed ? TChar::Concealed : TChar::None);
}

void TBuffer::decodeSGR38(const QStringList& parameters, bool isColonSeparated)
{
#if defined(DEBUG_SGR_PROCESSING)
    qDebug() << "    TBuffer::decodeSGR38(" << parameters << "," << isColonSeparated <<") INFO - called";
#endif
    if (parameters.at(1) == QLatin1String("5")) {
        int tag = 0;
        if (parameters.count() > 2) {
            bool isOk = false;
            tag = parameters.at(2).toInt(&isOk);
#if defined(DEBUG_SGR_PROCESSING)
            if (!isOk) {
                if (isColonSeparated) {
                    qDebug().noquote().nospace() << "TBuffer::decodeSGR38(...) ERROR - failed to parse color index parameter element (the third part) in a SGR...;38:5:" << parameters.at(2) << ":...;...m sequence treating it as a zero!";
                } else {
                    qDebug().noquote().nospace() << "TBuffer::decodeSGR38(...) ERROR - failed to parse color index parameter string (the third part) in a SGR...;38;5;" << parameters.at(2) << ";...m sequence treating it as a zero!";
                }
            }
#endif
        } else {
            // Missing last parameter - so it is treated as a zero
#if defined(DEBUG_SGR_PROCESSING)
            if (isColonSeparated) {
                qDebug().noquote().nospace() << "TBuffer::decodeSGR38(...) ERROR - missing color index parameter element (the third part) in a SGR...;38:5;...m sequence treating it as a zero!";
            } else {
                qDebug().noquote().nospace() << "TBuffer::decodeSGR38(...) ERROR - missing color index parameter string (the third part) in a SGR...;38;5;;m sequence treating it as a zero!";
            }
#endif
        }

        if (tag < 16) {
            if (tag >= 8) {
                tag -= 8;
                mBold = true;
            } else {
                mBold = false;
            }
            mIsDefaultColor = false;

            switch (tag) {
            case 0:
                mForeGroundColor = mBlack;
                mForeGroundColorLight = mLightBlack;
                break;
            case 1:
                mForeGroundColor = mRed;
                mForeGroundColorLight = mLightRed;
                break;
            case 2:
                mForeGroundColor = mGreen;
                mForeGroundColorLight = mLightGreen;
                break;
            case 3:
                mForeGroundColor = mYellow;
                mForeGroundColorLight = mLightYellow;
                break;
            case 4:
                mForeGroundColor = mBlue;
                mForeGroundColorLight = mLightBlue;
                break;
            case 5:
                mForeGroundColor = mMagenta;
                mForeGroundColorLight = mLightMagenta;
                break;
            case 6:
                mForeGroundColor = mCyan;
                mForeGroundColorLight = mLightCyan;
                break;
            case 7:
                mForeGroundColor = mWhite;
                mForeGroundColorLight = mLightWhite;
                break;
            }

        } else if (tag < 232) {
            // because color 1-15 behave like normal ANSI colors
           tag -= 16;
            // 6x6x6 RGB color space
            quint8 const r = tag / 36;
            quint8 const g = (tag - (r * 36)) / 6;
            quint8 const b = (tag - (r * 36)) - (g * 6);
            // Adjusted from previously linear gradient for the blocks.
            // To match the common terminal palettes, the values are
            // scaled as follows:
            // 0: 0, 1: 95, 2:135, 3:175, 4:215, 5:255
            mForeGroundColor = QColor(r == 0 ? 0 : (r - 1) * 40 + 95,
                                      g == 0 ? 0 : (g - 1) * 40 + 95,
                                      b == 0 ? 0 : (b - 1) * 40 + 95);
            mForeGroundColorLight = mForeGroundColor;

        } else {
            const int value = (tag - 232) * 10 + 8;
            mForeGroundColor = QColor(value, value, value);
            mForeGroundColorLight = mForeGroundColor;
        }

    } else if (parameters.at(1) == QLatin1String("2")) {
        if (parameters.count() >= 6) {
            // Have enough for all three colour
            // components
            mForeGroundColor = QColor(qBound(0, parameters.at(3).toInt(), 255), qBound(0, parameters.at(4).toInt(), 255), qBound(0, parameters.at(5).toInt(), 255));
        } else if (parameters.count() >= 5) {
            // Have enough for two colour
            // components, but blue component is
            // zero
            mForeGroundColor = QColor(qBound(0, parameters.at(3).toInt(), 255), qBound(0, parameters.at(4).toInt(), 255), 0);
        } else if (parameters.count() >= 4) {
            // Have enough for one colour component,
            // but green and blue components are
            // zero
            mForeGroundColor = QColor(qBound(0, parameters.at(3).toInt(), 255), 0, 0);
        } else  {
            // No codes left for any colour
            // components so colour must be black,
            // as all of red, green and blue
            // components are zero
            mForeGroundColor = Qt::black;
        }

        if (parameters.count() >= 3 && !parameters.at(2).isEmpty()) {
            if (!isColonSeparated) {
#if ! defined(DEBUG_SGR_PROCESSING)
                qDebug() << "Unhandled color space identifier in a SGR...;38;2;" << parameters.at(2) << ";...m sequence - if 16M colors items are missing blue elements you may have checked the \"Expect Color Space Id in SGR...(3|4)8;2;....m codes\" option on the Special Options tab of the preferences when it is not needed!";
#else
                qDebug().noquote().nospace() << "TBuffer::decodeSGR38(...) WARNING - unhandled color space identifier in a SGR...;38;2;" << parameters.at(2) << ";...m sequence treating it as the default (empty) case!";
            } else {
                qDebug().noquote().nospace() << "TBuffer::decodeSGR38(...) WARNING - unhandled color space identifier in a SGR...;38:2:" << parameters.at(2) << ":...;...m sequence treating it as the default (empty) case!";
#endif
            }
        }
        mForeGroundColorLight = mForeGroundColor;

    } else if (parameters.at(1) == QLatin1String("4")
            || parameters.at(1) == QLatin1String("3")
            || parameters.at(1) == QLatin1String("1")
            || parameters.at(1) == QLatin1String("0")) {

#if defined(DEBUG_SGR_PROCESSING)
        if (isColonSeparated) {
            qDebug().noquote().nospace() << "TBuffer::decodeSGR38(...) WARNING - unhandled SGR code: SGR...;38:" << parameters.at(1) << ":...;...m ignoring sequence!";
        } else {
            qDebug().noquote().nospace() << "TBuffer::decodeSGR38(...) WARNING - unhandled SGR code: SGR...;38;" << parameters.at(1) << ";...m ignoring sequence!";
        }
#endif

    } else {

#if defined(DEBUG_SGR_PROCESSING)
        if (isColonSeparated) {
            qDebug().noquote().nospace() << "TBuffer::decodeSGR38(...) WARNING - unexpected SGR code: SGR...;38:" << parameters.at(1) << ":...;...m ignoring sequence!";
        } else {
            qDebug().noquote().nospace() << "TBuffer::decodeSGR38(...) WARNING - unexpected SGR code: SGR...;38;" << parameters.at(1) << ";...m ignoring sequence!";
        }
#endif

    }
}

void TBuffer::decodeSGR48(const QStringList& parameters, bool isColonSeparated)
{
#if defined(DEBUG_SGR_PROCESSING)
    qDebug() << "    TBuffer::decodeSGR48(" << parameters << "," << isColonSeparated <<") INFO - called";
#endif
    bool useLightColor = false;

    if (parameters.at(1) == QLatin1String("5")) {
        int tag = 0;
        if (parameters.count() > 2) {
            bool isOk = false;
            tag = parameters.at(2).toInt(&isOk);
#if defined(DEBUG_SGR_PROCESSING)
            if (!isOk) {
                if (isColonSeparated) {
                    qDebug().noquote().nospace() << "TBuffer::decodeSGR48(...) ERROR - failed to parse color index parameter element (the third part) in a SGR...;48:5:" << parameters.at(2) << ":...;...m sequence treating it as a zero!";
                } else {
                    qDebug().noquote().nospace() << "TBuffer::decodeSGR48(...) ERROR - failed to parse color index parameter string (the third part) in a SGR...;48;5;" << parameters.at(2) << ";...m sequence treating it as a zero!";
                }
            }
#endif
        } else {
            // Missing last parameter - so it is treated as a zero
#if defined(DEBUG_SGR_PROCESSING)
            if (isColonSeparated) {
                qDebug().noquote().nospace() << "TBuffer::decodeSGR48(...) ERROR - missing color index parameter element (the third part) in a SGR...;48:5;...m sequence treating it as a zero!";
            } else {
                qDebug().noquote().nospace() << "TBuffer::decodeSGR48(...) ERROR - missing color index parameter string (the third part) in a SGR...;48;5;;m sequence treating it as a zero!";
            }
#endif
        }

        if (tag < 16) {
            if (tag >= 8) {
                tag -= 8;
                useLightColor = true;
            } else {
                useLightColor = false;
            }
            mIsDefaultColor = false;
            QColor bgColorLight;

            switch (tag) {
            case 0:
                mBackGroundColor = mBlack;
                bgColorLight = mLightBlack;
                break;
            case 1:
                mBackGroundColor = mRed;
                bgColorLight = mLightRed;
                break;
            case 2:
                mBackGroundColor = mGreen;
                bgColorLight = mLightGreen;
                break;
            case 3:
                mBackGroundColor = mYellow;
                bgColorLight = mLightYellow;
                break;
            case 4:
                mBackGroundColor = mBlue;
                bgColorLight = mLightBlue;
                break;
            case 5:
                mBackGroundColor = mMagenta;
                bgColorLight = mLightMagenta;
                break;
            case 6:
                mBackGroundColor = mCyan;
                bgColorLight = mLightCyan;
                break;
            case 7:
                mBackGroundColor = mWhite;
                bgColorLight = mLightWhite;
                break;
            }
            if (useLightColor) {
                mBackGroundColor = bgColorLight;
            }

        } else if (tag < 232) {
            // because color 1-15 behave like normal ANSI colors
            tag -= 16;
            // 6x6x6 RGB color space
            quint8 const r = tag / 36;
            quint8 const g = (tag - (r * 36)) / 6;
            quint8 const b = (tag - (r * 36)) - (g * 6);
            // Adjusted from previously linear gradient for the blocks.
            // To match the common terminal palettes, the values are
            // scaled as follows:
            // 0: 0, 1: 95, 2:135, 3:175, 4:215, 5:255
            mBackGroundColor = QColor(r == 0 ? 0 : (r - 1) * 40 + 95,
                                      g == 0 ? 0 : (g - 1) * 40 + 95,
                                      b == 0 ? 0 : (b - 1) * 40 + 95);

        } else {
            const int value = (tag - 232) * 10 + 8;
            mBackGroundColor = QColor(value, value, value);
        }

    } else if (parameters.at(1) == QLatin1String("2")) {
        if (parameters.count() >= 6) {
            // Have enough for all three colour
            // components
            mBackGroundColor = QColor(qBound(0, parameters.at(3).toInt(), 255), qBound(0, parameters.at(4).toInt(), 255), qBound(0, parameters.at(5).toInt(), 255));

        } else if (parameters.count() >= 5) {
            // Have enough for two colour
            // components, but blue component is
            // zero
            mBackGroundColor = QColor(qBound(0, parameters.at(3).toInt(), 255), qBound(0, parameters.at(4).toInt(), 255), 0);

        } else if (parameters.count() >= 4) {
            // Have enough for one colour component,
            // but green and blue components are
            // zero
            mBackGroundColor = QColor(qBound(0, parameters.at(3).toInt(), 255), 0, 0);

        } else  {
            // No codes left for any colour
            // components so colour must be black,
            // as all of red, green and blue
            // components are zero
            mBackGroundColor = Qt::black;
        }

        if (parameters.count() >= 3 && !parameters.at(2).isEmpty()) {
            if (!isColonSeparated) {
#if ! defined(DEBUG_SGR_PROCESSING)
                qDebug() << "Unhandled color space identifier in a SGR...;48;2;" << parameters.at(2) << ";...m sequence - if 16M colors items are missing blue elements you may have checked the \"Expect Color Space Id in SGR...(3|4)8;2;....m codes\" option on the Special Options tab of the preferences when it is not needed!";
#else
                qDebug().noquote().nospace() << "TBuffer::decodeSGR48(...) WARNING - unhandled color space identifier in a SGR...;48;2;" << parameters.at(2) << ";...m sequence treating it as the default (empty) case!";
            } else {
                qDebug().noquote().nospace() << "TBuffer::decodeSGR48(...) WARNING - unhandled color space identifier in a SGR...;48:2:" << parameters.at(2) << ":...;...m sequence treating it as the default (empty) case!";
#endif
            }
        }

    } else if (parameters.at(1) == QLatin1String("4")
            || parameters.at(1) == QLatin1String("3")
            || parameters.at(1) == QLatin1String("1")
            || parameters.at(1) == QLatin1String("0")) {

#if defined(DEBUG_SGR_PROCESSING)
        if (isColonSeparated) {
            qDebug().noquote().nospace() << "TBuffer::decodeSGR48(...) WARNING - unhandled SGR code: SGR...;48:" << parameters.at(1) << ":...;...m ignoring sequence!";
        } else {
            qDebug().noquote().nospace() << "TBuffer::decodeSGR48(...) WARNING - unhandled SGR code: SGR...;48;" << parameters.at(1) << ";...m ignoring sequence!";
        }
#endif

    } else {

#if defined(DEBUG_SGR_PROCESSING)
        if (isColonSeparated) {
            qDebug().noquote().nospace() << "TBuffer::decodeSGR48(...) WARNING - unexpected SGR code: SGR...;48:" << parameters.at(1) << ":...;...m ignoring sequence!";
        } else {
            qDebug().noquote().nospace() << "TBuffer::decodeSGR48(...) WARNING - unexpected SGR code: SGR...;48;" << parameters.at(1) << ";...m ignoring sequence!";
        }
#endif
    }

}

void TBuffer::decodeSGR(const QString& sequence)
{
    Host* pHost = mpHost;
    if (!pHost) {
        qWarning() << "TBuffer::decodeSGR(...) ERROR - Called when mpHost pointer is nullptr";
        return;
    }

    const bool haveColorSpaceId = pHost->getHaveColorSpaceId();

    const QStringList parameterStrings = sequence.split(QChar(';'));
    for (int paraIndex = 0, total = parameterStrings.count(); paraIndex < total; ++paraIndex) {
        const QString allParameterElements = parameterStrings.at(paraIndex);
        if (allParameterElements.contains(QLatin1String(":"))) {
            /******************************************************************
             * Parameter string with colon separated Parameter (sub) elements *
             ******************************************************************/
            // We have colon separated parameter elements, so we must have at least 2 members
            const QStringList parameterElements(allParameterElements.split(QChar(':')));
            if (parameterElements.at(0) == QLatin1String("38")) {
                if (parameterElements.count() >= 2) {
                    decodeSGR38(parameterElements, true);

                } else {
                    // We only have a single element in this parameterString,
                    // so we will need to steal the needed number from the
                    // remainder - this is falling back to using a semicolon
                    // separated list rather than a colon separated one
                    if (paraIndex + 1 >= total) {
                        // Oh dear we are out of parameters to examine, so bail
                        // out:
                        return;
                    }

                    // Okay we have one more parameter at least - so examine it
                    // and grab the needed number of arguments:
                    QStringList madeElements;
                    madeElements << parameterStrings.at(paraIndex); // "38"
                    madeElements << parameterStrings.at(paraIndex + 1); // "2" or "5" hopefully
                    bool isOk = false;
                    const int sgr38_type = madeElements.at(1).toInt(&isOk);
                    if (madeElements.at(1).isEmpty() || !isOk || sgr38_type == 0) {
                        // Oh dear that parameter is empty or equivalent to zero
                        // so we cannot do anything more
                        return;
                    }

                    switch (sgr38_type) {
                    case 5: // Needs just one more number
                        if (paraIndex + 2 < total) {
                            // We have the parameter needed
                            madeElements << parameterStrings.at(paraIndex + 2);
                        }
                        decodeSGR38(madeElements, false);
                        // Move the index to consume the used values
                        paraIndex += 2;
                        break;
                    case 4: // Not handled but we still should skip its arguments
                            // Uses four or five depending on whether there is
                            // the colour space id first
                            // Move the index to consume the used values
                        paraIndex += (haveColorSpaceId ? 6 : 5);
                        break;
                    case 3: // Not handled but we still should skip its arguments
                            // Move the index to consume the used values
                        paraIndex += (haveColorSpaceId ? 5 : 4);
                        break;
                    case 2: // Need three or four depending on whether there is
                            // the colour space id first
                        if (haveColorSpaceId) {
                            if (paraIndex + 2 < total) {
                                // We have the color space id
                                madeElements << parameterStrings.at(paraIndex + 2);
                            }
                            if (paraIndex + 3 < total) {
                                // We have the red component
                                madeElements << parameterStrings.at(paraIndex + 3);
                            }
                            if (paraIndex + 4 < total) {
                                // We have the green component
                                madeElements << parameterStrings.at(paraIndex + 4);
                            }
                            if (paraIndex + 5 < total) {
                                // We have the blue component
                                madeElements << parameterStrings.at(paraIndex + 5);
                            }
                        } else {
                            // Fake an empty colour space id
                            madeElements << QString();
                            if (paraIndex + 2 < total) {
                                // We have the red component
                                madeElements << parameterStrings.at(paraIndex + 2);
                            }
                            if (paraIndex + 3 < total) {
                                // We have the green component
                                madeElements << parameterStrings.at(paraIndex + 3);
                            }
                            if (paraIndex + 4 < total) {
                                // We have the blue component
                                madeElements << parameterStrings.at(paraIndex + 4);
                            }
                        }

                        decodeSGR38(madeElements, false);
                        // Move the index to consume the used values
                        paraIndex += (haveColorSpaceId ? 5 : 4);
                        break;
                    case 1: // This uses no extra arguments and, as it means
                            // transparent, is no use to us
                        [[fallthrough]];
                    default:
                        break;
                    }

                }
            // End of if (parameterElements.at(0) == QLatin1String("38"))
            } else if (parameterElements.at(0) == QLatin1String("48")) {
                if (parameterElements.count() >= 2) {
                    decodeSGR48(parameterElements, true);

                } else {
                    // We only have a single element in this parameterString,
                    // so we will need to steal the needed number from the
                    // remainder - this is falling back to using a semicolon
                    // separated list rather than a colon separated one
                    if (paraIndex + 1 >= total) {
                        // Oh dear we are out of parameters to examine, so bail
                        // out:
                        return;
                    }

                    // Okay we have one more parameter at least - so examine it
                    // and grab the needed number of arguments:
                    QStringList madeElements;
                    madeElements << parameterStrings.at(paraIndex);
                    madeElements << parameterStrings.at(paraIndex + 1);
                    bool isOk = false;
                    const int sgr48_type = madeElements.at(1).toInt(&isOk);
                    if (madeElements.at(1).isEmpty() || !isOk || sgr48_type == 0) {
                        // Oh dear that parameter is empty or equivalent to zero
                        // so we cannot do anything more
                        return;
                    }

                    switch (sgr48_type) {
                    case 5: // Needs one more number
                        if (paraIndex + 2 < total) {
                            // We have the parameter needed
                            madeElements << parameterStrings.at(paraIndex + 2);
                        }
                        // Move the index to consume the used values
                        decodeSGR48(madeElements, false);
                        paraIndex += 2;
                        break;
                    case 4: // Not handled but we still should skip its arguments
                            // Uses four or five depending on whether there is
                            // the colour space id first
                            // Move the index to consume the used values
                        paraIndex += (haveColorSpaceId ? 6 : 5);
                        break;
                    case 3: // Not handled but we still should skip its arguments
                            // Move the index to consume the used values
                        paraIndex += (haveColorSpaceId ? 5 : 4);
                        break;
                    case 2: // Need three or four depending on whether there is
                            // the colour space id first
                        if (haveColorSpaceId) {
                            if (paraIndex + 2 < total) {
                                // We have the color space id
                                madeElements << parameterStrings.at(paraIndex + 2);
                            }
                            if (paraIndex + 3 < total) {
                                // We have the red component
                                madeElements << parameterStrings.at(paraIndex + 3);
                            }
                            if (paraIndex + 4 < total) {
                                // We have the green component
                                madeElements << parameterStrings.at(paraIndex + 4);
                            }
                            if (paraIndex + 5 < total) {
                                // We have the blue component
                                madeElements << parameterStrings.at(paraIndex + 5);
                            }
                        } else {
                            // Fake an empty colour space id
                            madeElements << QString();
                            if (paraIndex + 2 < total) {
                                // We have the red component
                                madeElements << parameterStrings.at(paraIndex + 2);
                            }
                            if (paraIndex + 3 < total) {
                                // We have the green component
                                madeElements << parameterStrings.at(paraIndex + 3);
                            }
                            if (paraIndex + 4 < total) {
                                // We have the blue component
                                madeElements << parameterStrings.at(paraIndex + 4);
                            }
                        }

                        // Move the index to consume the used values
                        decodeSGR48(madeElements, false);
                        paraIndex += (haveColorSpaceId ? 5 : 4);
                        break;
                    case 1: // This uses no extra arguments and, as it means
                            // transparent, is no use to us
                        [[fallthrough]];
                    default:
                        break;
                    }

                }
            // End of if (parameterElements.at(0) == QLatin1String("48"))
            } else if (parameterElements.at(0) == QLatin1String("4")) {
                // New way of controlling underline
                bool isOk = false;
                const int value = parameterElements.at(1).toInt(&isOk);
                if (!isOk) {
                    // missing value
                    qDebug().noquote().nospace() << "TBuffer::decodeSGR(\"" << sequence << "\") ERROR - failed to detect underline parameter element (the second part) in a SGR...;4:?;..m sequence assuming it is a zero!";
                }
                switch (value) {
                case 0: // Underline off
                    mUnderline = false;
                    mUnderlineWavy = false;
                    mUnderlineDotted = false;
                    mUnderlineDashed = false;
                    break;
                case 1: // Underline on (solid)
                    mUnderline = true;
                    mUnderlineWavy = false;
                    mUnderlineDotted = false;
                    mUnderlineDashed = false;
                    break;
                case 2: // Dashed underline
                    mUnderline = true;
                    mUnderlineWavy = false;
                    mUnderlineDotted = false;
                    mUnderlineDashed = true;
                    break;
                case 3: // Dotted underline
                    mUnderline = true;
                    mUnderlineWavy = false;
                    mUnderlineDotted = true;
                    mUnderlineDashed = false;
                    break;
                case 4: // Wavy underline
                    mUnderline = true;
                    mUnderlineWavy = true;
                    mUnderlineDotted = false;
                    mUnderlineDashed = false;
                    break;
                default: // Something unexpected
                    qDebug().noquote().nospace() << "TBuffer::decodeSGR(\"" << sequence << "\") ERROR - unexpected underline parameter element (the second part) in a SGR...;4:" << parameterElements.at(1) << ";../m sequence treating it as a zero!";
                    mUnderline = false;
                    mUnderlineWavy = false;
                    mUnderlineDotted = false;
                    mUnderlineDashed = false;
                    break;
                }
            } else if (parameterElements.at(0) == QLatin1String("3")) {
                // New way of controlling italics
                bool isOk = false;
                const int value = parameterElements.at(1).toInt(&isOk);
                if (!isOk) {
                    // missing value
                    qDebug().noquote().nospace() << "TBuffer::decodeSGR(\"" << sequence << "\") ERROR - failed to detect italic parameter element (the second part) in a SGR...;3:?;../m sequence assuming it is a zero!";
                }
                switch (value) {
                case 0: // Italics/Slant off
                    mItalics = false;
                    break;
                case 1: // Italics on
                    mItalics = true;
                    break;
                case 2: // Slant on - not supported, treat as italics
                    qDebug().noquote().nospace() << "TBuffer::decodeSGR(\"" << sequence << "\") ERROR - unsupported italic parameter element (the second part) in a SGR...;3:" << parameterElements.at(1) << ";../m sequence treating it as a one!";
                    mUnderline = true;
                    break;
                default: // Something unexpected
                    qDebug().noquote().nospace() << "TBuffer::decodeSGR(\"" << sequence << "\") ERROR - unexpected italic parameter element (the second part) in a SGR...;3:" << parameterElements.at(1) << ";../m sequence treating it as a zero!";
                    mUnderline = false;
                    break;
                }
            } else {
                qDebug().noquote().nospace() << "TBuffer::decodeSGR(\"" << sequence << "\") ERROR - parameter string with an unexpected initial parameter element in a SGR...;" << parameterElements.at(0) << ":" << parameterElements.at(1) << "...;.../m sequence, ignoring it!";
            }
        } else {
            /******************************************************************
             *             Parameter string with no sub-elements              *
             ******************************************************************/
            // We do not have a colon separated string so we must just have a
            // number:
            bool isOk = false;
            int tag = 0;
            if (!allParameterElements.isEmpty()) {
                tag = allParameterElements.toInt(&isOk);
            } else {
                // Allow for an empty parameter to be treated as valid and equal to 0:
                isOk = true;
            }
            if (isOk) {
                switch (tag) {
                case 0:
                    mIsDefaultColor = true;
                    mForeGroundColor = pHost->mFgColor;
                    mBackGroundColor = pHost->mBgColor;
                    mBold = false;
                    mItalics = false;
                    mOverline = false;
                    mReverse = false;
                    mStrikeOut = false;
                    mUnderline = false;
                    mUnderlineWavy = false;
                    mUnderlineDotted = false;
                    mUnderlineDashed = false;
                    mBlink = false;
                    mFastBlink = false;
                    mConcealed = false;
                    mAltFont = 0;
                    break;
                case 1:
                    mBold = true;
                    break;
                case 2:
                    // Technically this should be faint (i.e. decreased
                    // intensity compared to normal and 22 should be
                    // the reset to "normal" intensity):
                    mBold = false;
                    break;
                case 3:
                    // There is a proposal by the "VTE" terminal
                    // emulator to use a (sub)parameter entry to
                    // distinguish between italics and slanted text by
                    // using ESC[...;3:1;...m and ESC[...;3:2;...m
                    // respectively - that is handled above in the colon
                    // sub-string separated part:
                    mItalics = true;
                    break;
                case 4:
                    // There is a implementation by some terminal
                    // emulators ("Kitty" and "VTE") to use a
                    // (sub)parameter entry of 3 for a wavy underline
                    // {presumably 2 would be a double underline and 1
                    // the normal single underline) by sending e.g.:
                    // ESC[...;4:3;...m - that is handled above in the colon
                    // sub-string separated part:
                    mUnderline = true;
                    break;
                case 5:
                    mBlink = true;
                    mFastBlink = false;
                    break; //slow-blinking, display as italics instead for the moment
                case 6:
                    mBlink = false;
                    mFastBlink = true;
                    break; //fast blinking, display as italics instead for the moment
                case 7:
                    mReverse = true;
                    break;
                case 8: // Concealed characters (set foreground to be the same as background?)
                    mConcealed = true;
                    break;
                case 9:
                    mStrikeOut = true;
                    break;
                case 10: //default font
                    mAltFont = 0;
                    break;
                case 11: // 11 to 19 are alternate fonts, what and where those
                         // are set is not so well specified
                    mAltFont = 1;
                    break;
                case 12:
                    mAltFont = 2;
                    break;
                case 13:
                    mAltFont = 3;
                    break;
                case 14:
                    mAltFont = 4;
                    break;
                case 15:
                    mAltFont = 5;
                    break;
                case 16:
                    mAltFont = 6;
                    break;
                case 17:
                    mAltFont = 7;
                    break;
                case 18:
                    mAltFont = 8;
                    break;
                case 19:
                    mAltFont = 9;
                    break;
                // case 21: // Double underline according to specs
                //    break;
                case 22:
                    mBold = false;
                    break;
                case 23:
                    mItalics = false;
                    break;
                case 24:
                    mUnderline = false;
                    mUnderlineWavy = false;
                    mUnderlineDotted = false;
                    mUnderlineDashed = false;
                    break;
                case 25:
                    mBlink = false;
                    mFastBlink = false;
                    break; // blink off
                case 27:
                    mReverse = false;
                    break;
                case 28: // Revealed characters (undoes the effect of "8")
                    mConcealed = false;
                    break;
                case 29:
                    mStrikeOut = false;
                    break;
                case 30:
                    mForeGroundColor = mBlack;
                    mForeGroundColorLight = mLightBlack;
                    mIsDefaultColor = false;
                    break;
                case 31:
                    mForeGroundColor = mRed;
                    mForeGroundColorLight = mLightRed;
                    mIsDefaultColor = false;
                    break;
                case 32:
                    mForeGroundColor = mGreen;
                    mForeGroundColorLight = mLightGreen;
                    mIsDefaultColor = false;
                    break;
                case 33:
                    mForeGroundColor = mYellow;
                    mForeGroundColorLight = mLightYellow;
                    mIsDefaultColor = false;
                    break;
                case 34:
                    mForeGroundColor = mBlue;
                    mForeGroundColorLight = mLightBlue;
                    mIsDefaultColor = false;
                    break;
                case 35:
                    mForeGroundColor = mMagenta;
                    mForeGroundColorLight = mLightMagenta;
                    mIsDefaultColor = false;
                    break;
                case 36:
                    mForeGroundColor = mCyan;
                    mForeGroundColorLight = mLightCyan;
                    mIsDefaultColor = false;
                    break;
                case 37:
                    mForeGroundColor = mWhite;
                    mForeGroundColorLight = mLightWhite;
                    mIsDefaultColor = false;
                    break;
                case 38: {
                    // We only have single elements so we will need to steal the
                    // needed number from the remainder:
                    if (paraIndex + 1 >= total) {
                        // Oh dear we are out of parameters to examine, so bail
                        // out:
                        return;
                    }

                    // Okay we have one more parameter at least - so examine it
                    // and grab the needed number of arguments:
                    QStringList madeElements;
                    madeElements << parameterStrings.at(paraIndex);
                    madeElements << parameterStrings.at(paraIndex + 1);
                    bool isOk = false;
                    const int sgr38_type = madeElements.at(1).toInt(&isOk);
                    if (madeElements.at(1).isEmpty() || !isOk || sgr38_type == 0) {
                        // Oh dear that parameter is empty or equivalent to zero
                        // so we cannot do anything more
                        return;
                    }

                    switch (sgr38_type) {
                    case 5: // Needs one more number
                        if (paraIndex + 2 < total) {
                            // We have the parameter needed
                            madeElements << parameterStrings.at(paraIndex + 2);
                        }
                        // Move the index to consume the used values
                        decodeSGR38(madeElements, false);
                        paraIndex += 2;
                        break;
                    case 4: // Not handled but we still should skip its arguments
                            // Uses four or five depending on whether there is
                            // the colour space id first
                            // Move the index to consume the used values
                        paraIndex += (haveColorSpaceId ? 6 : 5);
                        break;
                    case 3: // Not handled but we still should skip its arguments
                            // Move the index to consume the used values
                        paraIndex += (haveColorSpaceId ? 5 : 4);
                        break;
                    case 2: // Need three or four depending on whether there is
                            // the colour space id first
                        if (haveColorSpaceId) {
                            if (paraIndex + 2 < total) {
                                // We have the color space id
                                madeElements << parameterStrings.at(paraIndex + 2);
                            }
                            if (paraIndex + 3 < total) {
                                // We have the red component
                                madeElements << parameterStrings.at(paraIndex + 3);
                            }
                            if (paraIndex + 4 < total) {
                                // We have the green component
                                madeElements << parameterStrings.at(paraIndex + 4);
                            }
                            if (paraIndex + 5 < total) {
                                // We have the blue component
                                madeElements << parameterStrings.at(paraIndex + 5);
                            }
                        } else {
                            // Fake an empty colour space id
                            madeElements << QString();
                            if (paraIndex + 2 < total) {
                                // We have the red component
                                madeElements << parameterStrings.at(paraIndex + 2);
                            }
                            if (paraIndex + 3 < total) {
                                // We have the green component
                                madeElements << parameterStrings.at(paraIndex + 3);
                            }
                            if (paraIndex + 4 < total) {
                                // We have the blue component
                                madeElements << parameterStrings.at(paraIndex + 4);
                            }
                        }

                        // Move the index to consume the used values LESS
                        // the one that the for loop will handle - even if it
                        // goes past end
                        decodeSGR38(madeElements, false);
                        paraIndex += (haveColorSpaceId ? 5 : 4);
                        break;
                    case 1: // This uses no extra arguments and, as it means
                            // transparent, is no use to us
                        [[fallthrough]];
                    default:
                        break;
                    }
                }
                    break;
                case 39: //default foreground color
                    mForeGroundColor = pHost->mFgColor;
                    break;
                case 40:
                    mBackGroundColor = mBlack;
                    break;
                case 41:
                    mBackGroundColor = mRed;
                    break;
                case 42:
                    mBackGroundColor = mGreen;
                    break;
                case 43:
                    mBackGroundColor = mYellow;
                    break;
                case 44:
                    mBackGroundColor = mBlue;
                    break;
                case 45:
                    mBackGroundColor = mMagenta;
                    break;
                case 46:
                    mBackGroundColor = mCyan;
                    break;
                case 47:
                    mBackGroundColor = mWhite;
                    break;
                case 48: {
                    // We only have single elements so we will need to steal the
                    // needed number from the remainder:
                    if (paraIndex + 1 >= total) {
                        // Oh dear we are out of parameters to examine, so bail
                        // out:
                        return;
                    }

                    // Okay we have one more parameter at least - so examine it
                    // and grab the needed number of arguments:
                    QStringList madeElements;
                    madeElements << parameterStrings.at(paraIndex);
                    madeElements << parameterStrings.at(paraIndex + 1);
                    bool isOk = false;
                    const int sgr48_type = madeElements.at(1).toInt(&isOk);
                    if (madeElements.at(1).isEmpty() || !isOk || sgr48_type == 0) {
                        // Oh dear that parameter is empty or equivalent to zero
                        // so we cannot do anything more
                        return;
                    }

                    switch (sgr48_type) {
                    case 5: // Needs one more number
                        if (paraIndex + 2 < total) {
                            // We have the parameter needed
                            madeElements << parameterStrings.at(paraIndex + 2);
                        }
                        // Move the index to consume the used values
                        decodeSGR48(madeElements, false);
                        paraIndex += 2;
                        break;
                    case 4: // Not handled but we still should skip its arguments
                            // Uses four or five depending on whether there is
                            // the colour space id first
                            // Move the index to consume the used values
                        paraIndex += (haveColorSpaceId ? 6 : 5);
                        break;
                    case 3: // Not handled but we still should skip its arguments
                            // Move the index to consume the used values
                        paraIndex += (haveColorSpaceId ? 5 : 4);
                        break;
                    case 2: // Need three or four depending on whether there is
                            // the colour space id first
                        if (haveColorSpaceId) {
                            if (paraIndex + 2 < total) {
                                // We have the color space id
                                madeElements << parameterStrings.at(paraIndex + 2);
                            }
                            if (paraIndex + 3 < total) {
                                // We have the red component
                                madeElements << parameterStrings.at(paraIndex + 3);
                            }
                            if (paraIndex + 4 < total) {
                                // We have the green component
                                madeElements << parameterStrings.at(paraIndex + 4);
                            }
                            if (paraIndex + 5 < total) {
                                // We have the blue component
                                madeElements << parameterStrings.at(paraIndex + 5);
                            }
                        } else {
                            // Fake an empty colour space id
                            madeElements << QString();
                            if (paraIndex + 2 < total) {
                                // We have the red component
                                madeElements << parameterStrings.at(paraIndex + 2);
                            }
                            if (paraIndex + 3 < total) {
                                // We have the green component
                                madeElements << parameterStrings.at(paraIndex + 3);
                            }
                            if (paraIndex + 4 < total) {
                                // We have the blue component
                                madeElements << parameterStrings.at(paraIndex + 4);
                            }
                        }

                        // Move the index to consume the used values
                        decodeSGR48(madeElements, false);
                        paraIndex += (haveColorSpaceId ? 5 : 4);
                        break;
                    case 1: // This uses no extra arguments and, as it means
                            // transparent, is no use to us
                        [[fallthrough]];
                    default:
                        break;
                    }
                }
                    break;
                case 49: // default background color
                    mBackGroundColor = pHost->mBgColor;
                    break;
                // case 51: // Framed
                //    break;
                // case 52: // Encircled
                //    break;
                case 53:
                    mOverline = true;
                    break;
                // case 54: // Not framed, not encircled
                //    break;
                case 55:
                    mOverline = false;
                    break;
                // 56 to 59 reserved for future standardization
                // case 60: // ideogram underline or right side line
                //    break;
                // case 61: // ideogram double underline or double right side line
                //    break;
                // case 62: // ideogram overline or left side line
                //    break;
                // case 63: // ideogram double overline or double left side line
                //    break;
                // case 64: // ideogram stress marking
                //    break;
                // case 65: // cancels the effects of 60 to 64
                //    break;
                case 90:
                    mForeGroundColor = mLightBlack;
                    mForeGroundColorLight = mLightBlack;
                    mIsDefaultColor = false;
                    break;
                case 91:
                    mForeGroundColor = mLightRed;
                    mForeGroundColorLight = mLightRed;
                    mIsDefaultColor = false;
                    break;
                case 92:
                    mForeGroundColor = mLightGreen;
                    mForeGroundColorLight = mLightGreen;
                    mIsDefaultColor = false;
                    break;
                case 93:
                    mForeGroundColor = mLightYellow;
                    mForeGroundColorLight = mLightYellow;
                    mIsDefaultColor = false;
                    break;
                case 94:
                    mForeGroundColor = mLightBlue;
                    mForeGroundColorLight = mLightBlue;
                    mIsDefaultColor = false;
                    break;
                case 95:
                    mForeGroundColor = mLightMagenta;
                    mForeGroundColorLight = mLightMagenta;
                    mIsDefaultColor = false;
                    break;
                case 96:
                    mForeGroundColor = mLightCyan;
                    mForeGroundColorLight = mLightCyan;
                    mIsDefaultColor = false;
                    break;
                case 97:
                    mForeGroundColor = mLightWhite;
                    mForeGroundColorLight = mLightWhite;
                    mIsDefaultColor = false;
                    break;
                case 100:
                    mBackGroundColor = mLightBlack;
                    break;
                case 101:
                    mBackGroundColor = mLightRed;
                    break;
                case 102:
                    mBackGroundColor = mLightGreen;
                    break;
                case 103:
                    mBackGroundColor = mLightYellow;
                    break;
                case 104:
                    mBackGroundColor = mLightBlue;
                    break;
                case 105:
                    mBackGroundColor = mLightMagenta;
                    break;
                case 106:
                    mBackGroundColor = mLightCyan;
                    break;
                case 107:
                    mBackGroundColor = mLightWhite;
                    break;
                default:
                    qDebug().noquote().nospace() << "TBuffer::translateToPlainText(...) INFO - Unhandled single SGR code sequence CSI " << tag << " m received, Mudlet will ignore it.";
                }
            }
        }
    }
}

void TBuffer::decodeOSC(const QString& sequence)
{
    Host* pHost = mpHost;
    if (!pHost) {
        qWarning() << "TBuffer::decodeOSC(...) ERROR - Called when mpHost pointer is nullptr";
        return;
    }

    const bool serverMayRedefineDefaultColors = pHost->getMayRedefineColors();
#if defined(DEBUG_OSC_PROCESSING)
    // Only log sequences that aren't just OSC 8 terminators to reduce noise
    if (!(sequence.startsWith("8;;") && sequence.length() == 3)) {
        qDebug().nospace().noquote() << "    Consider the OSC sequence: \"" << sequence << "\"";
    }
#endif
    unsigned short const character = sequence.at(0).unicode();
    switch (character) {
    case static_cast<quint8>('P'):
        if (serverMayRedefineDefaultColors) {
            if (sequence.size() == 8) {
                // Should be a 8 byte Hex number in form PIRRGGBB - including the 'P'
                bool isOk = false;
                // Uses mid(...) rather than at(...) because we want the return to
                // be a (single character) QString and not a QChar so we can use
                // QString::toUInt(...):
                quint8 const colorNumber = sequence.mid(1, 1).toUInt(&isOk, 16);
                quint8 rr = 0;
                if (isOk) {
                    rr = sequence.mid(2, 2).toUInt(&isOk, 16);
                }
                quint8 gg = 0;
                if (isOk) {
                    gg = sequence.mid(4, 2).toUInt(&isOk, 16);
                }
                quint8 bb = 0;
                if (isOk) {
                    bb = sequence.mid(6, 2).toUInt(&isOk, 16);
                }
                if (isOk) {
                    bool isValid = true;
                    switch (colorNumber) {
                    case 0: // Black
                        pHost->mBlack = QColor(rr, gg, bb);
                        break;
                    case 1: // Red
                        pHost->mRed = QColor(rr, gg, bb);
                        break;
                    case 2: // Green
                        pHost->mGreen = QColor(rr, gg, bb);
                        break;
                    case 3: // Yellow
                        pHost->mYellow = QColor(rr, gg, bb);
                        break;
                    case 4: // Blue
                        pHost->mBlue = QColor(rr, gg, bb);
                        break;
                    case 5: // Magenta
                        pHost->mMagenta = QColor(rr, gg, bb);
                        break;
                    case 6: // Cyan
                        pHost->mCyan = QColor(rr, gg, bb);
                        break;
                    case 7: // Light gray
                        pHost->mWhite = QColor(rr, gg, bb);
                        break;
                    case 8: // Dark gray
                        pHost->mLightBlack = QColor(rr, gg, bb);
                        break;
                    case 9: // Light Red
                        pHost->mLightRed = QColor(rr, gg, bb);
                        break;
                    case 10: // Light Green
                        pHost->mLightGreen = QColor(rr, gg, bb);
                        break;
                    case 11: // Light Yellow
                        pHost->mLightYellow = QColor(rr, gg, bb);
                        break;
                    case 12: // Light Blue
                        pHost->mLightBlue = QColor(rr, gg, bb);
                        break;
                    case 13: // Light Magenta
                        pHost->mLightMagenta = QColor(rr, gg, bb);
                        break;
                    case 14: // Light Cyan
                        pHost->mLightCyan = QColor(rr, gg, bb);
                        break;
                    case 15: // Light gray
                        pHost->mLightWhite = QColor(rr, gg, bb);
                        break;
                    default:
                        isValid = false;
                    }
                    if (isValid) {
                        // This will refresh the "main" console as it is only this
                        // class instance associated with that one that is to be
                        // changed by this method:
                        if (pHost->mpConsole) {
                            pHost->mpConsole->changeColors();
                        }
                        // Also need to update the Lua sub-system's "color_table"
                        pHost->updateAnsi16ColorsInTable();
                    }

                } else {
#if defined(DEBUG_OSC_PROCESSING)
                    qDebug().noquote().nospace() << "TBuffer::decodeOSC(\"" << sequence << "\") ERROR - Unable to parse this as a <OSC>P<I><RR><GG><BB><ST> string to redefined one of the 16 ANSI colors.";
#endif
                }
            } else {
#if defined(DEBUG_OSC_PROCESSING)
                qDebug().noquote().nospace() << "TBuffer::decodeOSC(\"" << sequence << "\") ERROR - Wrong length of string, unable to decode this as a <OSC>P<I><RR><GG><BB><ST> string to redefined one of the 16 ANSI colors.";
#endif
            }
        }
        break;
    case static_cast<quint8>('R'):
        if (serverMayRedefineDefaultColors) {
            resetColors();
        }
        break;
    case static_cast<quint8>('8'): {
        // Handle OSC 8 hyperlinks in the form: "8;params;URI"

        QStringView rest = QStringView(sequence).mid(1);  // skip selector "8;"
        int firstSemi = rest.indexOf(';');

        if (firstSemi == -1) {
            qWarning() << "OSC 8: Missing first semicolon";
            return;
        }

        int secondSemi = rest.indexOf(';', firstSemi + 1);

        if (secondSemi == -1) {
            qWarning() << "OSC 8: Missing second semicolon";
            return;
        }

        QString param = rest.left(firstSemi).toString();

#if defined(DEBUG_OSC_PROCESSING)
        if (!param.isEmpty()) {
            qDebug().noquote().nospace() << "[OSC 8] Params provided (not used by Mudlet but shown for debugging): \"" << param << "\"";
        }
#endif
        QString rawUrl = rest.mid(secondSemi + 1).toString();

        // OSC 8 ;; closes the hyperlink
        if ((param.isEmpty() && rawUrl.isEmpty())) {
#if defined(DEBUG_OSC_PROCESSING)
            qDebug().noquote() << "[OSC8] Hyperlink terminator - closing active hyperlink";
#endif
            mCurrentHyperlinkCommand.clear();
            mCurrentHyperlinkHint.clear();
            mCurrentHyperlinkLinkId = 0;
            mHyperlinkActive = false;
            // Reset enhanced styling
            mCurrentHyperlinkStyling = Mudlet::HyperlinkStyling();
            mCurrentHyperlinkMenu.clear();
            break;
        }

        if (!rawUrl.isEmpty()) {
            if (rawUrl.length() > 8192) {
                qWarning() << "TBuffer::decodeOSC(...) - Rejected hyperlink: URL too long:" << rawUrl;
                return;
            }

            // Parse query parameters for enhanced functionality
#if defined(DEBUG_OSC_PROCESSING)
            qDebug() << "[OSC8] Raw URL for parameter parsing:" << rawUrl;
#endif
            QMap<QString, QString> queryParams = parseUriQueryParameters(rawUrl);

            // Extract styling parameters
            if (queryParams.contains(qsl("style"))) {
#if defined(DEBUG_OSC_PROCESSING)
                qDebug() << "[OSC8] Found style parameter, applying custom styling";
#endif
                parseHyperlinkStyling(queryParams.value(qsl("style")), mCurrentHyperlinkStyling);
            } else {
                mCurrentHyperlinkStyling = Mudlet::HyperlinkStyling(); // Reset to defaults
#if defined(DEBUG_OSC_PROCESSING)
                qDebug() << "[OSC8] No style parameter provided, using defaults (isUnderlined=" << mCurrentHyperlinkStyling.isUnderlined << ")";
#endif
            }

            // Extract menu parameters
            if (queryParams.contains(qsl("menu"))) {
                QString menuString = queryParams.value(qsl("menu"));
                mCurrentHyperlinkMenu = menuString.split('|', Qt::SkipEmptyParts);
#if defined(DEBUG_OSC_PROCESSING)
                qDebug() << "[OSC8] Menu parameter found:" << menuString;
                qDebug() << "[OSC8] Menu items parsed:" << mCurrentHyperlinkMenu;
                qDebug() << "[OSC8] Menu items count:" << mCurrentHyperlinkMenu.size();
#endif
            } else {
                mCurrentHyperlinkMenu.clear();
#if defined(DEBUG_OSC_PROCESSING)
                qDebug() << "[OSC8] No menu parameter found";
#endif
            }

            // Extract custom tooltip parameter
            QString customTooltip;

            if (queryParams.contains(qsl("tooltip"))) {
                customTooltip = queryParams.value(qsl("tooltip"));
            }

            // Remove styling/menu/tooltip query parameters from URL for command processing
            QString baseUrl = rawUrl;
            QMap<QString, QString> allParams = parseUriQueryParameters(rawUrl);

            // For web URLs, preserve original parameters except our special ones
            if (rawUrl.startsWith(qsl("http://")) || rawUrl.startsWith(qsl("https://")) || rawUrl.startsWith(qsl("ftp://"))) {
                // Remove our special parameters
                allParams.remove(qsl("style"));
                allParams.remove(qsl("menu"));
                allParams.remove(qsl("tooltip"));

                // Rebuild URL with only non-special parameters
                int queryStart = baseUrl.indexOf('?');
                if (queryStart != -1) {
                    baseUrl = baseUrl.left(queryStart);
                }

                // Only append parameters if there are any left
                if (!allParams.isEmpty()) {
                    baseUrl = appendQueryParameters(baseUrl, allParams);
                }
            } else {
                // For send: and prompt: commands, remove all query parameters
                int queryStart = baseUrl.indexOf('?');
                if (queryStart != -1) {
                    baseUrl = baseUrl.left(queryStart);
                }
            }

            QStringList command;
            QStringList hint;

            if (baseUrl.startsWith(qsl("send:"))) {
                QString innerCommand = QUrl::fromPercentEncoding(baseUrl.mid(5).toUtf8());
                command = { qsl("send([[%1]])").arg(innerCommand) };
                hint = { qsl("%1: %2").arg(QObject::tr("Send"), innerCommand) };
            } else if (baseUrl.startsWith(qsl("prompt:"))) {
                QString innerCommand = QUrl::fromPercentEncoding(baseUrl.mid(7).toUtf8());
                command = { qsl("sendCmdLine([[%1]])").arg(innerCommand) };
                hint = { qsl("%1: %2").arg(QObject::tr("Prompt"), innerCommand) };
            } else {
                QUrl qurl(baseUrl);
                QString scheme = qurl.scheme().toLower();

                if (scheme == qsl("http") || scheme == qsl("https") || scheme == qsl("ftp")) {
                    command = { qsl("openUrl([[%1]])").arg(baseUrl) };
                    hint = { qsl("%1: %2").arg(QObject::tr("Open browser to"), baseUrl) };
                } else {
                    qWarning().noquote().nospace() << "TBuffer::decodeOSC(...) - Ignored untrusted or unsupported URI scheme: \"" << scheme << "\"";
                    return;
                }
            }

            // Add standalone tooltip support (for links without menus)
            if (!customTooltip.isEmpty() && (mCurrentHyperlinkMenu.isEmpty() || mCurrentHyperlinkMenu.size() < 2)) {
                // Replace the default hint with the custom tooltip
                hint = { customTooltip };
#if defined(DEBUG_OSC_PROCESSING)
                qDebug() << "[OSC8] Added standalone tooltip:" << customTooltip;
#endif
            }

            // Handle menu functionality by extending commands and hints
            if (!mCurrentHyperlinkMenu.isEmpty() && mCurrentHyperlinkMenu.size() >= 2) {
#if defined(DEBUG_OSC_PROCESSING)
                qDebug() << "[OSC8] Building menu commands from" << mCurrentHyperlinkMenu.size() << "menu items";
#endif
                QStringList menuCommands;
                QStringList menuHints;

                // Add menu items in pairs (label, command)
                // The first menu item becomes the primary left-click action (index 0)
                // All items (including first) appear in the right-click menu (index 1+)
                for (int i = 0; i < mCurrentHyperlinkMenu.size() - 1; i += 2) {
                    QString menuLabel = mCurrentHyperlinkMenu[i];
                    QString menuCommand = mCurrentHyperlinkMenu[i + 1];

                    // Determine command type based on prefix
                    if (menuCommand.startsWith(qsl("send:"))) {
                        QString innerCommand = QUrl::fromPercentEncoding(menuCommand.mid(5).toUtf8());
                        menuCommands.append(qsl("send([[%1]])").arg(innerCommand));
                        menuHints.append(menuLabel);
                    } else if (menuCommand.startsWith(qsl("prompt:"))) {
                        QString innerCommand = QUrl::fromPercentEncoding(menuCommand.mid(7).toUtf8());
                        menuCommands.append(qsl("sendCmdLine([[%1]])").arg(innerCommand));
                        menuHints.append(menuLabel);
                    } else if (menuCommand == qsl("-")) {
                        // Special case: "-" creates a menu separator
                        menuCommands.append(QString());
                        menuHints.append(QString());
                    } else {
                        // Treat as direct command
                        menuCommands.append(qsl("send([[%1]])").arg(menuCommand));
                        menuHints.append(menuLabel);
                    }
                }

                // Set the tooltip for the link (what shows on hover)
                // Use custom tooltip if provided, otherwise show "Right-click for menu" message
                QString linkTooltip;
                if (!customTooltip.isEmpty()) {
                    // Custom tooltip explicitly provided
                    linkTooltip = customTooltip;
                } else {
                    // Default tooltip for menu links
                    linkTooltip = QObject::tr("Right-click for menu");
                }

                // For menus, we need tooltip as first hint, then menu labels
                // This ensures tooltip.size() > commands.size() so only first hint shows in tooltip
                // But menu labels are available for right-click menu display
                QStringList finalHints;
                finalHints.append(linkTooltip); // First: tooltip for hover
                finalHints.append(menuHints);   // Rest: menu labels for right-click menu

                command = menuCommands;
                hint = finalHints;
#if defined(DEBUG_OSC_PROCESSING)
                qDebug() << "[OSC8] Final menu commands:" << command;
                qDebug() << "[OSC8] Final menu hints:" << hint;
#endif
            }

            mCurrentHyperlinkCommand = command;
            mCurrentHyperlinkHint = hint;
            mCurrentHyperlinkLinkId = mLinkStore.addLinks(command, hint, mpHost, QVector<int>());

            // Store the styling for this link so it can be retrieved later
            mLinkStore.setStyling(mCurrentHyperlinkLinkId, mCurrentHyperlinkStyling);

            // Store the original background color for this link so we can restore it later
            // when the link styling doesn't specify a background
            mLinkOriginalBackgrounds[mCurrentHyperlinkLinkId] = mBackGroundColor;

#if defined(DEBUG_OSC_PROCESSING)
            qDebug().noquote() << "[OSC8] Hyperlink activated:" << rawUrl.left(50) + (rawUrl.length() > 50 ? "..." : "");
#endif
            mHyperlinkActive = true;
        }
        break;
    }
    default:
        qDebug().noquote().nospace() << "TBuffer::decodeOSC(\"" << sequence << "\") ERROR - Unhandled <OSC>?...<ST> code, Mudlet will ignore it.";
    }
}

QString TBuffer::appendQueryParameters(const QString& uri, const QMap<QString, QString>& parameters)
{
    if (parameters.isEmpty()) {
        return uri;
    }

    QString result = uri;
    bool hasExistingParams = uri.contains(qsl("?"));
    QString separator = hasExistingParams ? qsl("&") : qsl("?");

    QStringList paramStrings;
    for (auto it = parameters.constBegin(); it != parameters.constEnd(); ++it) {
        QString key = QUrl::toPercentEncoding(it.key());
        QString value = QUrl::toPercentEncoding(it.value());
        paramStrings.append(qsl("%1=%2").arg(key, value));
    }

    if (!paramStrings.isEmpty()) {
        result += separator + paramStrings.join(qsl("&"));
    }

    return result;
}

QMap<QString, QString> TBuffer::parseUriQueryParameters(const QString& uri)
{
    QMap<QString, QString> parameters;

#if defined(DEBUG_OSC_PROCESSING)
    qDebug() << "[OSC8] parseUriQueryParameters called with uri:" << uri;
#endif

    // Find the query string part after '?'
    int queryStart = uri.indexOf('?');
    if (queryStart == -1) {
#if defined(DEBUG_OSC_PROCESSING)
        qDebug() << "[OSC8] No query parameters found in URI";
#endif
        return parameters; // No query parameters
    }

    QString queryString = uri.mid(queryStart + 1);
#if defined(DEBUG_OSC_PROCESSING)
    qDebug() << "[OSC8] Query string:" << queryString;
#endif

    // Decode the query string first, since the server percent-encodes the entire URL
    QString decodedQueryString = QUrl::fromPercentEncoding(queryString.toUtf8());
#if defined(DEBUG_OSC_PROCESSING)
    qDebug() << "[OSC8] Decoded query string:" << decodedQueryString;
#endif

    // Only process JSON config parameters - no legacy CSS support
    if (decodedQueryString.startsWith(qsl("config={"))) {
        QString jsonString = decodedQueryString.mid(7); // Remove "config="
        parseJsonHyperlinkConfig(jsonString, parameters);
    }

#if defined(DEBUG_OSC_PROCESSING)
    qDebug() << "[OSC8] Parsed JSON parameters:" << parameters;
#endif

    return parameters;
}

bool TBuffer::parseJsonHyperlinkConfig(const QString& jsonString, QMap<QString, QString>& parameters)
{
#if defined(DEBUG_OSC_PROCESSING)
    qDebug() << "[OSC8] parseJsonHyperlinkConfig called with jsonString:" << jsonString;
#endif

    QJsonParseError parseError;
    QJsonDocument doc = QJsonDocument::fromJson(jsonString.toUtf8(), &parseError);

    if (parseError.error != QJsonParseError::NoError) {
#if defined(DEBUG_OSC_PROCESSING)
        qDebug() << "[OSC8] JSON parse error:" << parseError.errorString();
#endif
        return false;
    }

    if (!doc.isObject()) {
#if defined(DEBUG_OSC_PROCESSING)
        qDebug() << "[OSC8] JSON root is not an object";
#endif
        return false;
    }

    QJsonObject root = doc.object();

    // Parse style object
    if (root.contains(qsl("style")) && root[qsl("style")].isObject()) {
        QJsonObject styleObj = root[qsl("style")].toObject();
        QString cssStyleString = jsonStyleObjectToCss(styleObj);
        if (!cssStyleString.isEmpty()) {
            parameters.insert(qsl("style"), cssStyleString);
        }
    }

    // Parse menu array
    if (root.contains(qsl("menu")) && root[qsl("menu")].isArray()) {
        QJsonArray menuArray = root[qsl("menu")].toArray();
        QString menuString = jsonMenuArrayToString(menuArray);
        if (!menuString.isEmpty()) {
            parameters.insert(qsl("menu"), menuString);
        }
    }

    // Parse tooltip string
    if (root.contains(qsl("tooltip")) && root[qsl("tooltip")].isString()) {
        parameters.insert(qsl("tooltip"), root[qsl("tooltip")].toString());
    }

#if defined(DEBUG_OSC_PROCESSING)
    qDebug() << "[OSC8] JSON converted to parameters:" << parameters;
#endif

    return true;
}

QString TBuffer::jsonStyleObjectToCss(const QJsonObject& styleObj)
{
    QStringList cssProperties;

    // Basic style properties
    if (styleObj.contains(qsl("color")) && styleObj[qsl("color")].isString()) {
        cssProperties << qsl("color:") + styleObj[qsl("color")].toString();
    }

    if (styleObj.contains(qsl("bg")) && styleObj[qsl("bg")].isString()) {
        cssProperties << qsl("background-color:") + styleObj[qsl("bg")].toString();
    }

    if (styleObj.contains(qsl("bold")) && styleObj[qsl("bold")].isBool() && styleObj[qsl("bold")].toBool()) {
        cssProperties << qsl("font-weight:bold");
    }

    if (styleObj.contains(qsl("italic")) && styleObj[qsl("italic")].isBool() && styleObj[qsl("italic")].toBool()) {
        cssProperties << qsl("font-style:italic");
    }

    // Text decorations - combine multiple decorations into single CSS property
    QStringList decorationParts;
    QString underlineStyle = qsl("solid"); // Default underline style

    // Process underline
    if (styleObj.contains(qsl("underline"))) {
        QJsonValue underlineVal = styleObj[qsl("underline")];
        if (underlineVal.isBool() && underlineVal.toBool()) {
            decorationParts << qsl("underline");
        } else if (underlineVal.isString()) {
            decorationParts << qsl("underline");
            underlineStyle = underlineVal.toString(); // Store style for later
        }
    }

    // Process overline
    if (styleObj.contains(qsl("overline"))) {
        QJsonValue overlineVal = styleObj[qsl("overline")];
        if (overlineVal.isBool() && overlineVal.toBool()) {
            decorationParts << qsl("overline");
        } else if (overlineVal.isString()) {
            decorationParts << qsl("overline");
            // Note: overline styles are currently limited to solid in the parser
        }
    }

    // Process strikethrough
    if (styleObj.contains(qsl("strikethrough"))) {
        QJsonValue strikeVal = styleObj[qsl("strikethrough")];
        if (strikeVal.isBool() && strikeVal.toBool()) {
            decorationParts << qsl("line-through");
        } else if (strikeVal.isString()) {
            decorationParts << qsl("line-through");
            // Note: strikethrough styles are currently limited to solid in the parser
        }
    }

    // Combine all decorations into a single CSS property
    if (!decorationParts.isEmpty()) {
        QString decorationValue = decorationParts.join(qsl(" "));
        // Add underline style if it's not solid and underline is present
        if (decorationParts.contains(qsl("underline")) && underlineStyle != qsl("solid")) {
            decorationValue += qsl(" ") + underlineStyle;
        }
        cssProperties << qsl("text-decoration:") + decorationValue;
    }

    // Text decoration color
    if (styleObj.contains(qsl("text-decoration-color")) && styleObj[qsl("text-decoration-color")].isString()) {
        cssProperties << qsl("text-decoration-color:") + styleObj[qsl("text-decoration-color")].toString();
    }

    // Pseudo-class states
    QStringList pseudoClasses = {qsl("hover"), qsl("active"), qsl("visited"), qsl("link"), qsl("focus"), qsl("focus-visible"), qsl("any-link")};

    for (const QString& pseudoClass : pseudoClasses) {
        if (styleObj.contains(pseudoClass) && styleObj[pseudoClass].isObject()) {
            QJsonObject pseudoObj = styleObj[pseudoClass].toObject();
            QString pseudoCss = jsonStyleObjectToCss(pseudoObj);
            if (!pseudoCss.isEmpty()) {
                cssProperties << qsl(":") + pseudoClass + qsl("{") + pseudoCss + qsl("}");
            }
        }
    }

    return cssProperties.join(qsl(";"));
}

QString TBuffer::jsonMenuArrayToString(const QJsonArray& menuArray)
{
    QStringList menuItems;

    for (const QJsonValue& item : menuArray) {
        if (item.isString() && item.toString() == qsl("-")) {
            // Separator
            menuItems << qsl("Separator|-");
        } else if (item.isObject()) {
            QJsonObject menuObj = item.toObject();
            // Each menu object should have one key-value pair: label -> command
            for (auto it = menuObj.begin(); it != menuObj.end(); ++it) {
                if (it.value().isString()) {
                    menuItems << it.key() + qsl("|") + it.value().toString();
                }
            }
        }
    }

    return menuItems.join(qsl("|"));
}

void TBuffer::parseHyperlinkStyling(const QString& styleString, Mudlet::HyperlinkStyling& styling)
{
#if defined(DEBUG_OSC_PROCESSING)
    qDebug() << "[OSC8] parseHyperlinkStyling called with styleString:" << styleString;
#endif

    // Reset styling to defaults
    styling = Mudlet::HyperlinkStyling();

    // Check if this is a CSS selector with pseudo-classes
    // Format: "property:value;:pseudo-class{property:value;property:value}:pseudo-class{...}"
    // or just: ":pseudo-class{property:value;property:value}:pseudo-class{...}"
    if (styleString.contains('{') && styleString.contains('}')) {
        // First, extract and parse any base properties (properties not in pseudo-classes)
        // These are properties that come before any pseudo-class or between pseudo-classes
        QString baseProperties;
        static const QRegularExpression pseudoClassRegex(R"(:([\w-]+)\s*\{([^}]*)\})");

        // Extract base properties by removing all pseudo-class blocks
        QString remainingStyle = styleString;
        QRegularExpressionMatchIterator matches = pseudoClassRegex.globalMatch(styleString);

        // Remove pseudo-class blocks to get base properties
        while (matches.hasNext()) {
            QRegularExpressionMatch match = matches.next();
            remainingStyle.replace(match.captured(0), "");
        }

        // Parse base properties if any exist
        // Filter out strings that are only semicolons and whitespace
        QString filteredBase = remainingStyle;
        filteredBase.remove(';');
        filteredBase = filteredBase.trimmed();

        if (!filteredBase.isEmpty()) {
            baseProperties = remainingStyle.trimmed();
#if defined(DEBUG_OSC_PROCESSING)
            qDebug() << "[OSC8] Found base properties:" << baseProperties;
#endif
            // Mark that we have base styling (not just pseudo-class styling)
            styling.hasBaseCustomStyling = true;

            // Parse base properties as simple CSS
            QStringList basePairs = baseProperties.split(';', Qt::SkipEmptyParts);

            for (const QString& pair : basePairs) {
                QStringList propertyValue = pair.split(':', Qt::KeepEmptyParts);

                if (propertyValue.size() != 2) {
                    continue;
                }

                QString property = propertyValue[0].trimmed().toLower();
                QString value = propertyValue[1].trimmed();

                // Apply base properties to the default state
                if (property == qsl("color")) {
                    QColor color = parseColorValue(value);
                    if (color.isValid()) {
                        styling.foregroundColor = color;
                        styling.hasForegroundColor = true;
                        styling.hasCustomStyling = true;
                    }
                } else if (property == qsl("background-color") || property == qsl("background")) {
                    QColor color = parseColorValue(value);
                    if (color.isValid()) {
                        styling.backgroundColor = color;
                        styling.hasBackgroundColor = true;
                        styling.hasCustomStyling = true;
                    }
                } else if (property == qsl("font-weight")) {
                    styling.isBold = (value.toLower() == qsl("bold"));
                    styling.hasCustomStyling = true;
                } else if (property == qsl("font-style")) {
                    styling.isItalic = (value.toLower() == qsl("italic"));
                    styling.hasCustomStyling = true;
                }
            }
        }

        // Now parse pseudo-class specific styles
        matches = pseudoClassRegex.globalMatch(styleString);

        while (matches.hasNext()) {
            QRegularExpressionMatch match = matches.next();
            QString pseudoClass = ":" + match.captured(1);
            QString properties = match.captured(2);

#if defined(DEBUG_OSC_PROCESSING)
            qDebug() << "[OSC8] Found pseudo-class:" << pseudoClass << "with properties:" << properties;
#endif

            parseHyperlinkStateStyle(pseudoClass, properties, styling);
        }

        // If no base styling was provided, but we have :link or :any-link styles,
        // use them as the default/base state since links need a default appearance
        if (!styling.hasCustomStyling) {
            // First try :any-link (applies to both :link and :visited)
            if (styling.anyLinkStyle.hasCustomStyling) {
#if defined(DEBUG_OSC_PROCESSING)
                qDebug() << "[OSC8] No base styling found, using :any-link as default";
#endif
                styling.foregroundColor = styling.anyLinkStyle.foregroundColor;
                styling.backgroundColor = styling.anyLinkStyle.backgroundColor;
                styling.underlineColor = styling.anyLinkStyle.underlineColor;
                styling.overlineColor = styling.anyLinkStyle.overlineColor;
                styling.strikeoutColor = styling.anyLinkStyle.strikeoutColor;
                styling.hasForegroundColor = styling.anyLinkStyle.hasForegroundColor;
                styling.hasBackgroundColor = styling.anyLinkStyle.hasBackgroundColor;
                styling.hasUnderlineColor = styling.anyLinkStyle.hasUnderlineColor;
                styling.hasOverlineColor = styling.anyLinkStyle.hasOverlineColor;
                styling.hasStrikeoutColor = styling.anyLinkStyle.hasStrikeoutColor;
                styling.isBold = styling.anyLinkStyle.isBold;
                styling.isItalic = styling.anyLinkStyle.isItalic;
                styling.isUnderlined = styling.anyLinkStyle.isUnderlined;
                styling.isStrikeOut = styling.anyLinkStyle.isStrikeOut;
                styling.isOverlined = styling.anyLinkStyle.isOverlined;
                styling.underlineStyle = styling.anyLinkStyle.underlineStyle;
                styling.hasCustomStyling = true;
            } else if (styling.linkStyle.hasCustomStyling) {
                // Fall back to :link style
#if defined(DEBUG_OSC_PROCESSING)
                qDebug() << "[OSC8] No base styling found, using :link as default";
#endif
                styling.foregroundColor = styling.linkStyle.foregroundColor;
                styling.backgroundColor = styling.linkStyle.backgroundColor;
                styling.underlineColor = styling.linkStyle.underlineColor;
                styling.overlineColor = styling.linkStyle.overlineColor;
                styling.strikeoutColor = styling.linkStyle.strikeoutColor;
                styling.hasForegroundColor = styling.linkStyle.hasForegroundColor;
                styling.hasBackgroundColor = styling.linkStyle.hasBackgroundColor;
                styling.hasUnderlineColor = styling.linkStyle.hasUnderlineColor;
                styling.hasOverlineColor = styling.linkStyle.hasOverlineColor;
                styling.hasStrikeoutColor = styling.linkStyle.hasStrikeoutColor;
                styling.isBold = styling.linkStyle.isBold;
                styling.isItalic = styling.linkStyle.isItalic;
                styling.isUnderlined = styling.linkStyle.isUnderlined;
                styling.isStrikeOut = styling.linkStyle.isStrikeOut;
                styling.isOverlined = styling.linkStyle.isOverlined;
                styling.underlineStyle = styling.linkStyle.underlineStyle;
                styling.hasCustomStyling = true;
            }
        }

        // Apply accessibility enhancements
        applyAccessibilityEnhancements(styling);
        return;
    }

    // Fallback: Parse as simple CSS-like style string: "property:value;property:value"
    QStringList stylePairs = styleString.split(';', Qt::SkipEmptyParts);

    bool hasAnyCustomStyling = false;
    bool textDecorationExplicitlySet = false;

    for (const QString& pair : stylePairs) {
        QStringList propertyValue = pair.split(':', Qt::KeepEmptyParts);

        if (propertyValue.size() != 2) {
            continue; // Skip malformed property
        }

        QString property = propertyValue[0].trimmed().toLower();
        QString value = propertyValue[1].trimmed();

        if (property == "color") {
            QColor color = parseColorValue(value);

            if (color.isValid()) {
                styling.foregroundColor = color;
                styling.hasForegroundColor = true;
                hasAnyCustomStyling = true;
            }
        } else if (property == "background-color") {
            QColor color = parseColorValue(value);

            if (color.isValid()) {
                styling.backgroundColor = color;
                styling.hasBackgroundColor = true;
                hasAnyCustomStyling = true;
            }
        } else if (property == "font-weight") {
            styling.isBold = (value.toLower() == qsl("bold"));
            hasAnyCustomStyling = true;
        } else if (property == "font-style") {
            styling.isItalic = (value.toLower() == qsl("italic"));
            hasAnyCustomStyling = true;
        } else if (property == "text-decoration") {
            // Handle text-decoration with optional style: "underline", "underline wavy", "overline", "line-through", "none"
            // Can also handle multiple values: "underline overline", "underline wavy red"
            QStringList decorationParts = value.toLower().split(' ', Qt::SkipEmptyParts);

            // Reset decoration flags when text-decoration is explicitly set
            styling.isUnderlined = false;
            styling.isOverlined = false;
            styling.isStrikeOut = false;
            styling.underlineStyle = Mudlet::HyperlinkStyling::UnderlineNone;

            for (const QString& part : decorationParts) {
                if (part == "underline") {
                    styling.isUnderlined = true;
                    styling.underlineStyle = Mudlet::HyperlinkStyling::UnderlineSolid;
                } else if (part == "overline") {
                    styling.isOverlined = true;
                } else if (part == "line-through") {
                    styling.isStrikeOut = true;
                } else if (part == "none") {
                    styling.isUnderlined = false;
                    styling.isOverlined = false;
                    styling.isStrikeOut = false;
                    styling.underlineStyle = Mudlet::HyperlinkStyling::UnderlineNone;
                } else if (part == "wavy" && styling.isUnderlined) {
                    styling.underlineStyle = Mudlet::HyperlinkStyling::UnderlineWavy;
                } else if (part == "dotted" && styling.isUnderlined) {
                    styling.underlineStyle = Mudlet::HyperlinkStyling::UnderlineDotted;
                } else if (part == "dashed" && styling.isUnderlined) {
                    styling.underlineStyle = Mudlet::HyperlinkStyling::UnderlineDashed;
                } else if (part == "solid" && styling.isUnderlined) {
                    styling.underlineStyle = Mudlet::HyperlinkStyling::UnderlineSolid;
                } else {
                    // Check if it's a color for the decoration
                    QColor decorationColor = parseColorValue(part);

                    if (decorationColor.isValid()) {
                        if (styling.isUnderlined) {
                            styling.underlineColor = decorationColor;
                            styling.hasUnderlineColor = true;
#if defined(DEBUG_OSC_PROCESSING)
                            qDebug() << "[OSC8] Set underline color to" << decorationColor.name();
#endif
                        } else if (styling.isOverlined) {
                            styling.overlineColor = decorationColor;
                            styling.hasOverlineColor = true;
#if defined(DEBUG_OSC_PROCESSING)
                            qDebug() << "[OSC8] Set overline color to" << decorationColor.name();
#endif
                        } else if (styling.isStrikeOut) {
                            styling.strikeoutColor = decorationColor;
                            styling.hasStrikeoutColor = true;
#if defined(DEBUG_OSC_PROCESSING)
                            qDebug() << "[OSC8] Set strikeout color to" << decorationColor.name();
#endif
                        }
                    }
                }
            }

            textDecorationExplicitlySet = true;
            hasAnyCustomStyling = true;
        } else if (property == "text-decoration-line") {
            // CSS3 specific property for decoration lines
            // Reset decoration flags when text-decoration-line is explicitly set
            styling.isUnderlined = false;
            styling.isOverlined = false;
            styling.isStrikeOut = false;

            QStringList decorations = value.toLower().split(' ', Qt::SkipEmptyParts);

            for (const QString& decoration : decorations) {
                if (decoration == qsl("underline")) {
                    styling.isUnderlined = true;
                } else if (decoration == qsl("overline")) {
                    styling.isOverlined = true;
                } else if (decoration == qsl("line-through")) {
                    styling.isStrikeOut = true;
                } else if (decoration == qsl("none")) {
                    styling.isUnderlined = false;
                    styling.isOverlined = false;
                    styling.isStrikeOut = false;
                }
            }

            textDecorationExplicitlySet = true;
            hasAnyCustomStyling = true;
        } else if (property == "text-decoration-style") {
            // CSS3 specific property for decoration style
            QString decorationStyle = value.toLower();

            if (decorationStyle == qsl("wavy")) {
                styling.underlineStyle = Mudlet::HyperlinkStyling::UnderlineWavy;
            } else if (decorationStyle == qsl("dotted")) {
                styling.underlineStyle = Mudlet::HyperlinkStyling::UnderlineDotted;
            } else if (decorationStyle == qsl("dashed")) {
                styling.underlineStyle = Mudlet::HyperlinkStyling::UnderlineDashed;
            } else if (decorationStyle == qsl("solid")) {
                styling.underlineStyle = Mudlet::HyperlinkStyling::UnderlineSolid;
            }
            hasAnyCustomStyling = true;
        } else if (property == "text-decoration-color") {
            // CSS3 specific property for decoration color
            QColor decorationColor = parseColorValue(value);

            if (decorationColor.isValid()) {
                // Store the color for all decoration types - we'll apply to active decorations later
                styling.underlineColor = decorationColor;
                styling.hasUnderlineColor = true;
                styling.overlineColor = decorationColor;
                styling.hasOverlineColor = true;
                styling.strikeoutColor = decorationColor;
                styling.hasStrikeoutColor = true;
            }

            hasAnyCustomStyling = true;
        }
    }

    // If custom styling was provided, mark it and disable default underline if text-decoration wasn't explicitly set
    if (hasAnyCustomStyling) {
        styling.hasCustomStyling = true;
        // Only disable default underline if text-decoration wasn't explicitly specified
        if (!textDecorationExplicitlySet) {
            styling.isUnderlined = false;
            styling.underlineStyle = Mudlet::HyperlinkStyling::UnderlineNone;
        }
    }
}

QColor TBuffer::parseColorValue(const QString& value)
{
    QString cleanValue = value.trimmed().toLower();

    // Handle hex colors: #rrggbb (6-digit) or #rgb (3-digit shorthand)
    // Qt's QColor constructor automatically expands 3-digit format (e.g., #f00 -> #ff0000)
    if (cleanValue.startsWith('#')) {
        QColor color(cleanValue);

        if (color.isValid()) {
            return color;
        }
    }

    // Handle named colors
    QColor namedColor(cleanValue);

    if (namedColor.isValid()) {
        return namedColor;
    }

    // Handle rgb() format: rgb(255, 0, 0) or rgb(100%, 0%, 0%)
    // Supports both integer (0-255) and percentage (0%-100%) values
    // Spaces after commas are optional: rgb(255,0,0) works too
    if (cleanValue.startsWith("rgb(") && cleanValue.endsWith(')')) {
        QString rgbContent = cleanValue.mid(4, cleanValue.length() - 5);
        QStringList components = rgbContent.split(',');

        if (components.size() == 3) {
            auto parseComponent = [](const QString& comp, bool& ok) -> int {
                QString trimmed = comp.trimmed();

                if (trimmed.endsWith('%')) {
                    // Percentage value: convert 0-100% to 0-255
                    trimmed.chop(1); // Remove '%'
                    double percent = trimmed.toDouble(&ok);

                    if (ok && percent >= 0.0 && percent <= 100.0) {
                        return qRound(percent * 2.55);
                    }

                    return -1;
                } else {
                    // Integer value: 0-255
                    int value = trimmed.toInt(&ok);

                    if (ok && value >= 0 && value <= 255) {
                        return value;
                    }
                    return -1;
                }
            };

            bool ok1, ok2, ok3;
            int r = parseComponent(components[0], ok1);
            int g = parseComponent(components[1], ok2);
            int b = parseComponent(components[2], ok3);

            if (ok1 && ok2 && ok3 && r >= 0 && g >= 0 && b >= 0) {
                return QColor(r, g, b);
            }
        }
    }

    return QColor(); // Invalid color
}

void TBuffer::resetColors()
{
    Host* pHost = mpHost;
    if (!pHost) {
        qWarning() << "TBuffer::resetColors(...) ERROR - Called when mpHost pointer is nullptr";
        return;
    }

    // These should match the corresponding settings in
    // dlgProfilePreferences::slot_resetColors() :
    pHost->mBlack = Qt::black;
    pHost->mLightBlack = Qt::darkGray;
    pHost->mRed = Qt::darkRed;
    pHost->mLightRed = Qt::red;
    pHost->mGreen = Qt::darkGreen;
    pHost->mLightGreen = Qt::green;
    pHost->mBlue = Qt::darkBlue;
    pHost->mLightBlue = Qt::blue;
    pHost->mYellow = Qt::darkYellow;
    pHost->mLightYellow = Qt::yellow;
    pHost->mCyan = Qt::darkCyan;
    pHost->mLightCyan = Qt::cyan;
    pHost->mMagenta = Qt::darkMagenta;
    pHost->mLightMagenta = Qt::magenta;
    pHost->mWhite = Qt::lightGray;
    pHost->mLightWhite = Qt::white;

    // This will refresh the "main" console as it is only this class instance
    // associated with that one that will call this method from the
    // decodeOSC(...) method:
    if (pHost->mpConsole) {
        pHost->mpConsole->changeColors();
    }

    // Also need to update the Lua sub-system's "color_table"
    pHost->updateAnsi16ColorsInTable();
}

void TBuffer::append(const QString& text, int sub_start, int sub_end, TChar format, int linkID)
{
    append(text, sub_start, sub_end, format.mFgColor, format.mBgColor, format.mFlags, linkID);
}

void TBuffer::append(const QString& text, int sub_start, int sub_end,
                     const QColor& fgColor, const QColor& bgColor,
                     TChar::AttributeFlags flags, int linkID)
{
    const int lastLineBeforeWrap = buffer.size() - 1;
    const int lastLineLength = lineBuffer.at(lastLineBeforeWrap).size();
    appendLine(text, sub_start, sub_end, fgColor, bgColor, flags, linkID);
    if (text.isEmpty()) {
        return;
    }
    // optimization: if the lastLine length hasn't changed,
    // skip it and wrap subsequent lines
    if (lastLineLength == lineBuffer.at(lastLineBeforeWrap).size()) {
        log(lastLineBeforeWrap, lastLineBeforeWrap);
        wrapLine(lastLineBeforeWrap + 1, mWrapAt, mWrapIndent, mWrapHangingIndent);
    } else {
        wrapLine(lastLineBeforeWrap, mWrapAt, mWrapIndent, mWrapHangingIndent);
    }
    if (static_cast<int>(buffer.size()) > mLinesLimit) {
        shrinkBuffer();
    }
    // Whilst shrinkBuffer() is used when the buffer exceeds a user defined
    // limit to prevent it growing beyond a "reasonable" size we also
    // want to check - for TConsoles that have been set to be "non-scrollable"
    // - that the content has not exceeded the number of lines that can be
    // shown in the upper pane and to raise an event if it has
    if (!mpConsole.isNull()) {
        mpConsole->handleLinesOverflowEvent(lineBuffer.size());
    }
}

void TBuffer::appendLine(const QString& text, const int sub_start, const int sub_end,
                         const QColor& fgColor, const QColor& bgColor,
                         const TChar::AttributeFlags flags, const int linkID)
{
    if (sub_end < 0) {
        return;
    }

    // Check for OSC 8 documentation examples trigger phrase
    if (text.contains("!osc8-docs")) {
#if defined(DEBUG_OSC_PROCESSING)
        qDebug() << "[OSC8] Documentation examples trigger phrase detected";
#endif
        injectOSC8DocumentationExamples();
        return; // Don't display the trigger phrase itself
    }

    int lastLine = buffer.size() - 1;
    if (Q_UNLIKELY(lastLine < 0)) {
        // There are NO lines in the buffer - so initialize with a new empty line
        appendEmptyLine();
        lastLine = 0;
        // The ternary operator is used here to set/reset only the TChar::Echo bit in the flags:
        const TChar styling(fgColor, bgColor,
                (mEchoingText ? (TChar::Echo | (flags & TChar::TestMask))
                 : (flags & TChar::TestMask)));
        buffer.back().push_back(styling);
    }

    if (text.isEmpty()) {
        return;
    }
    bool firstChar = (lineBuffer.back().isEmpty());
    const int length = std::min(static_cast<int>(text.size()), MAX_CHARACTERS_PER_ECHO);
    int lineEndPos = sub_end;
    if (lineEndPos >= length) {
        lineEndPos = text.size() - 1;
    }

    for (int i = sub_start; i <= (sub_start + lineEndPos); i++) {
        const QChar thisChar = text.at(i);
        if (thisChar == QChar::LineFeed) {
            firstChar = true;
            appendEmptyLine();
            continue;
        }
        lineBuffer.back().append(thisChar);
        const TChar styling(fgColor, bgColor, (mEchoingText ? (TChar::Echo | flags) : flags), linkID);
        buffer.back().push_back(styling);

        // Note: Original character storage for ANSI-styled OSC 8 links happens in
        // translateToPlainText() where the TChar is created with ANSI formatting
        // before CSS styling is applied

        if (firstChar) {
            timeBuffer.back() = QTime::currentTime().toString(mudlet::smTimeStampFormat);
            firstChar = false;
        }
    }
}

void TBuffer::appendEmptyLine()
{
    std::deque<TChar> const newLine;
    buffer.push_back(newLine);
    lineBuffer.push_back(QString());
    timeBuffer << QTime::currentTime().toString(mudlet::smTimeStampFormat);
    promptBuffer << false;
}

// This was called "insert" but that is commonly used for built in methods and
// it makes it harder to pick out usages of this specific method:
bool TBuffer::insertInLine(QPoint& P, const QString& text, const TChar& format)
{
    if (text.isEmpty()) {
        return false;
    }
    const int x = P.x();
    const int y = P.y();
    if ((y >= 0) && (y < static_cast<int>(buffer.size()))) {
        if (x < 0) {
            return false;
        }
        if (x >= static_cast<int>(buffer.at(y).size())) {
            TChar c(mpConsole);
            expandLine(y, x - buffer.at(y).size(), c);
        }
        for (int i = 0, total = text.size(); i < total; ++i) {
            lineBuffer[y].insert(x + i, text.at(i));
            const TChar c = format;
            auto it = buffer[y].begin();
            buffer[y].insert(it + x + i, c);
        }
    } else {
        appendLine(text, 0, text.size(), format.mFgColor, format.mBgColor, format.mFlags);
    }
    return true;
}

// This is very poorly designed as P2 is used to determine the last character to
// copy BUT no consideration is given to P2.y() != p1.y() i.e. a copy of more
// than a single line - and it copies a single QChar at a time....
TBuffer TBuffer::copy(QPoint& P1, QPoint& P2)
{
    TBuffer slice(mpHost);
    slice.clear();
    const int y = P1.y();
    int x = P1.x();
    if (y < 0 || y >= static_cast<int>(buffer.size())) {
        return slice;
    }

    // Ensure x starts within the valid range, and adjust P2.x() if it's out of bounds
    if (x < 0 || x >= static_cast<int>(buffer.at(y).size())) {
        x = 0; // Reset x to start of line if out of bounds
    }
    int P2x_corrected = std::min(P2.x(), static_cast<int>(buffer.at(y).size()) - 1); // Correct P2.x() to prevent out-of-bounds

    int oldLinkId{}, id{};
    for (; x <= P2x_corrected; ++x) {
        const int linkId = buffer.at(y).at(x).linkIndex();
        if (linkId && (linkId != oldLinkId)) {
            id = slice.mLinkStore.addLinks(mLinkStore.getLinksConst(linkId), mLinkStore.getHintsConst(linkId), mpHost);
            oldLinkId = linkId;
        }

        if (!linkId) {
            id = 0;
        }
        const QString s(lineBuffer.at(y).at(x));
        slice.append(s, 0, 1, buffer.at(y).at(x).mFgColor, buffer.at(y).at(x).mBgColor, buffer.at(y).at(x).mFlags, id);
    }
    return slice;
}

// This is constrained to P1.y() == P2.y()....
TBuffer TBuffer::cut(QPoint& P1, QPoint& P2)
{
    TBuffer slice = copy(P1, P2);
    TChar format(mpConsole);
    replaceInLine(P1, P2, QString(), format);
    return slice;
}

// This only copies the first line of chunk's contents:
void TBuffer::paste(QPoint& P, const TBuffer& chunk)
{
    const bool needAppend = false;
    bool hasAppended = false;
    int y = P.y();
    const int x = P.x();
    if (chunk.buffer.empty()) {
        return;
    }
    if (y < 0 || y > getLastLineNumber()) {
        y = getLastLineNumber();
    }
    // FIXME: RISK OF EXCEPTION getLastLineNumber() returns zero (not -1) if
    // the buffer is empty, so y can never be less than zero here - however that
    // will cause an exception with std::deque::at(size_t) - previously
    // std::deque::operator[size_t] was used and that exhibits UNDEFINED
    // BEHAVIOUR in the same situation:
    if (x < 0 || x >= static_cast<int>(buffer.at(y).size())) {
        return;
    }

    for (int cx = 0, total = static_cast<int>(chunk.buffer.at(0).size()); cx < total; ++cx) {
        // This is rather inefficient as s is only ever one QChar long
        QPoint P_current(cx, y);
        if ((y < getLastLineNumber()) && (!needAppend)) {
            const TChar& format = chunk.buffer.at(0).at(cx);
            const QString s = QString(chunk.lineBuffer.at(0).at(cx));
            insertInLine(P_current, s, format);
        } else {
            hasAppended = true;
            const QString s(chunk.lineBuffer.at(0).at(cx));
            append(s, 0, 1, chunk.buffer.at(0).at(cx).mFgColor, chunk.buffer.at(0).at(cx).mBgColor, chunk.buffer.at(0).at(cx).mFlags);
        }
    }

    if (hasAppended && y != -1) {
        TChar format(mpConsole);
        wrapLine(y, mWrapAt, mWrapIndent, mWrapHangingIndent);
    }
}

// This only appends the FIRST line of chunk:
void TBuffer::appendBuffer(const TBuffer& chunk)
{
    if (chunk.buffer.empty()) {
        return;
    }
    int oldLinkId{}, id{};
    for (int cx = 0, total = static_cast<int>(chunk.buffer.at(0).size()); cx < total; ++cx) {
        const int linkId = chunk.buffer.at(0).at(cx).linkIndex();
        if (linkId && (oldLinkId != linkId)) {
            id = mLinkStore.addLinks(chunk.mLinkStore.getLinksConst(linkId), chunk.mLinkStore.getHintsConst(linkId), mpHost);
            oldLinkId = linkId;
        }
        if (!linkId) {
            id = 0;
        }
        const QString s(chunk.lineBuffer.at(0).at(cx));
        append(s, 0, 1, chunk.buffer.at(0).at(cx).mFgColor, chunk.buffer.at(0).at(cx).mBgColor, chunk.buffer.at(0).at(cx).mFlags, id);
    }

    append(QString(QChar::LineFeed), 0, 1, Qt::black, Qt::black, TChar::None);
}

int TBuffer::calculateWrapPosition(int lineNumber, int begin, int end)
{
    const QString lineBreaks = qsl("- \n");
    if (lineBuffer.size() < lineNumber) {
        return 0;
    }
    const int lineSize = static_cast<int>(lineBuffer[lineNumber].size()) - 1;
    if (lineSize < end) {
        end = lineSize;
    }
    const auto line = lineBuffer[lineNumber];
    for (int i = end; i >= begin; --i) {
        if (lineBreaks.indexOf(line.at(i)) > -1) {
            return i;
        }
    }

    return lineSize;
}

inline int TBuffer::skipSpacesAtBeginOfLine(const int row, const int column)
{
    int offset = 0;
    int position = column;
    const int endOfLinePosition = lineBuffer.at(row).size();
    while (position < endOfLinePosition) {
        if (buffer.at(row).at(position).mFlags & TChar::Echo) {
            break;
        }
        if (lineBuffer.at(row).at(position) == QChar::Space) {
            ++offset;
        } else {
            break;
        }
        position++;
    }
    return offset;
}

// find lindbreaks and indents (if not necessary, return empty list)
inline QList<WrapInfo> TBuffer::getWrapInfo(const QString& lineText, bool isNewline,
    const int maxWidth, const int indent, const int hangingIndent)
{
    QList<WrapInfo> output;
    if (lineText.isEmpty()) {
        return output;
    }

    // Safety check: during destruction, mpHost might be null
    if (!mpHost) {
        // Return a simple wrap info without character width calculations
        output.append(WrapInfo(isNewline, false, 0, lineText.length()));
        return output;
    }

    QTextBoundaryFinder boundaryFinder(QTextBoundaryFinder::Grapheme, lineText);
    QTextBoundaryFinder lineBreakFinder(QTextBoundaryFinder::Line, lineText);
    int xPos = 0;
    int totalWidth = 0;
    int firstChar = 0;
    bool needsIndent = isNewline;

    // find all the appropriate wrap points assuming (hanging-)indentation prepended to each line
    for (int indexOfChar = 0, total = lineText.size(); indexOfChar < total and indexOfChar >= 0;) {
        const QChar c = lineText.at(indexOfChar);
        // skip leading spaces for any wrapped lines
        if (xPos == 0 and !isNewline and !output.isEmpty() and c == QChar::Space) {
            indexOfChar++;
            firstChar = indexOfChar;
            boundaryFinder.setPosition(indexOfChar);
            continue;
        }
        // handle embedded linefeed
        if (c == QChar::LineFeed) {
            output.append(WrapInfo(isNewline, needsIndent, firstChar, indexOfChar));
            indexOfChar++;
            boundaryFinder.setPosition(indexOfChar);
            firstChar = indexOfChar;
            isNewline = true;
            needsIndent = false;
            continue;
        }
        int nextBoundary = boundaryFinder.toNextBoundary();
        const QString grapheme = lineText.mid(indexOfChar, nextBoundary - indexOfChar);
        const uint unicode = graphemeInfo::getBaseCharacter(grapheme);
        // Safety check: during destruction, mpHost might be null
        const int charWidth = mpHost ? graphemeInfo::getWidth(unicode, mpHost->wideAmbiguousEAsianGlyphs())
                                     : graphemeInfo::getWidth(unicode, false);
        const int indentationHere = isNewline ? indent : hangingIndent;
        if (xPos + charWidth > maxWidth - (needsIndent ? indentationHere : 0)) {
            if (isNewline) {
                needsIndent = true;
            }
            lineBreakFinder.setPosition(indexOfChar);
            // we check c == QChar::Space since we are happy to break at -any- space,
            // unlike the indirect-linebreak permission of QTextBoundaryFinder::Line
            // (see: https://www.unicode.org/reports/tr14/#LD9) which will only break at
            // at the first char after 1+ space(s)
            const int firstNonIndentChar = firstChar + (needsIndent ? 0 : indentationHere);
            if (c == QChar::Space or lineBreakFinder.isAtBoundary() or lineBreakFinder.toPreviousBoundary() <= firstNonIndentChar) {
                boundaryFinder.setPosition(indexOfChar);
                output.append(WrapInfo(isNewline, needsIndent, firstChar, indexOfChar));
            } else {
                indexOfChar = lineBreakFinder.position();
                nextBoundary = lineBreakFinder.position();
                boundaryFinder.setPosition(nextBoundary);
                output.append(WrapInfo(isNewline, needsIndent, firstChar, indexOfChar));
            }
            isNewline = false;
            needsIndent = true;
            xPos = 0;
            firstChar = indexOfChar;
            continue;
        }
        xPos += charWidth;
        totalWidth += charWidth;
        indexOfChar = nextBoundary;
    }
    // it's possible that no wrapping is needed
    if (totalWidth <= mWrapAt) {
        output.clear();
        return output;
    }
    // if the line has been wrapped, append last remaining bit
    if (!output.isEmpty()) {
        output.append(WrapInfo(isNewline, !isNewline, firstChar, lineText.size()));
    }
    return output;
}

// This only works on the Main Console for a profile
void TBuffer::log(int fromLine, int toLine)
{
    if (mpHost.isNull()) {
        return;
    }

    TBuffer* pB = &mpHost->mpConsole->buffer;
    if (pB != this || !mpHost->mpConsole->mLogToLogFile) {
        return;
    }

    if (fromLine >= size() || fromLine < 0) {
        return;
    }
    if (toLine >= size()) {
        toLine = size() - 1;
    }
    if (toLine < 0) {
        return;
    }

    // if we've been called to log the same line - which can happen when the user
    // enters a command after in-game text - then skip recording the last line
    if (fromLine != lastLoggedFromLine && toLine != lastloggedToLine) {
        mpHost->mpConsole->mLogStream << lastTextToLog;
        mpHost->mpConsole->mLogStream.flush();
    }

    QStringList linesToLog;
    for (int i = fromLine; i <= toLine; ++i) {
        if (mpHost->mIsCurrentLogFileInHtmlFormat) {
            // This only handles a single line of logged text at a time:
            linesToLog << bufferToHtml(mpHost->mIsLoggingTimestamps, i);
        } else {
            linesToLog << ((mpHost->mIsLoggingTimestamps && !timeBuffer.at(i).isEmpty()) ? timeBuffer.at(i).left(mudlet::smTimeStampFormat.length()) : QString()) % lineBuffer.at(i) % QChar::LineFeed;
        }
    }

    // record the last log call into a temporary buffer - we'll actually log
    // on the next iteration after duplication detection has run
    lastTextToLog = linesToLog.join(QString());
    lastLoggedFromLine = fromLine;
    lastloggedToLine = toLine;
}

// logs the remaining output when logging gets stopped, without duplication checks
void TBuffer::logRemainingOutput()
{
    mpHost->mpConsole->mLogStream << lastTextToLog;
    mpHost->mpConsole->mLogStream.flush();
}

// logs a string directly to the log file
void TBuffer::appendLog(const QString &text)
{
    TBuffer* pB = &mpHost->mpConsole->buffer;
    if (pB != this || !mpHost->mpConsole->mLogToLogFile) {
        return;
    }

    mpHost->mpConsole->mLogStream << text;
}

// returns how many new lines have been inserted by the wrapping action
int TBuffer::wrapLine(int startLine, int maxWidth, int indentSize, int hangingIndentSize)
{
    if (static_cast<int>(buffer.size()) < startLine || startLine < 0) {
        return 0;
    }

    std::queue<std::deque<TChar>> queue;
    QStringList tempList;
    QStringList timeList;
    QList<bool> promptList;
    int lineCount = 0;
    // consider moving this upstream and returning an error if you try to set indentation higher than wrapWidth
    const int indent = (indentSize < maxWidth) ? indentSize : 0;
    const int hangingIndent = (hangingIndentSize < maxWidth) ? hangingIndentSize : 0;
    for (int i = startLine, total = static_cast<int>(buffer.size()); i < total; ++i) {
        lineCount++;
        std::deque<TChar> newBufferLine;
        QString newLineText;
        const QString time = timeBuffer[i];
        // trivial case
        if (buffer[i].size() == 0) {
            tempList.append(newLineText);
            queue.push(newBufferLine);
            timeList.append(time);
            promptList.append(false);
            continue;
        }

        // consider wrapping line
        int newBufferCharPosition = 0;
        const bool isPrompt = promptBuffer[i];
        const QString lineText = lineBuffer[i];
        // a blank timestamp indicates a wrapped line
        const bool isNewline = (time != mudlet::smBlankTimeStamp);
        QList<WrapInfo> lineBreaks = getWrapInfo(lineText, isNewline, maxWidth, indent, hangingIndent);
        if (lineBreaks.isEmpty()) {
            tempList.append(lineText);
            queue.push(buffer[i]);
            timeList.append(time);
            promptList.append(isPrompt);
            continue;
        }
        const QString qIndent(indent, QChar::Space);
        const QString qHangingIndent(hangingIndent, QChar::Space);
        for (WrapInfo w : lineBreaks) {
            // skip TChars as needed
            while (newBufferCharPosition < w.firstChar) {
                buffer[i].pop_front();
                newBufferCharPosition++;
            }
            if (w.needsIndent) {
                // background color of indentation spaces should match first char in the line
                const TChar indentSpace = buffer[i].front();
                // add indentation to TChar buffer and newLineText
                if (w.isNewline) {
                    for (int i = 0; i < indent; ++i) {
                        newBufferLine.push_front(indentSpace);
                    }
                    newLineText.append(qIndent);
                } else {
                    for (int i = 0; i < hangingIndent; ++i) {
                        newBufferLine.push_front(indentSpace);
                    }
                    newLineText.append(qHangingIndent);
                }
            }
            // append TChars of the wrapped lineText to TChar buffer
            while (newBufferCharPosition < w.lastChar) {
                newBufferLine.push_back(buffer[i].front());
                buffer[i].pop_front();
                newBufferCharPosition++;
            }
            // everything else
            newLineText.append(lineText.mid(w.firstChar, w.lastChar - w.firstChar));
            tempList.append(newLineText);
            if (w.isNewline) {
                timeList.append(time);
            } else {
                timeList.append(mudlet::smBlankTimeStamp);
            }
            queue.push(newBufferLine);
            promptList.append(isPrompt);
            newBufferLine.clear();
            newLineText = QString();
        }
    }
    for (int i = 0; i < lineCount; ++i) {
        buffer.pop_back();
        lineBuffer.pop_back();
        timeBuffer.pop_back();
        promptBuffer.pop_back();
    }

    const int insertedLines = queue.size() - 1;
    for (int i = 0; i <= insertedLines; ++i) {
        if (tempList[i].size() < 1) {
            queue.pop();
            appendEmptyLine();
        } else {
            buffer.push_back(queue.front());
            queue.pop();
            lineBuffer.append(tempList[i]);
            timeBuffer.append(timeList[i]);
            promptBuffer.push_back(promptList[i]);
        }
    }

    if (insertedLines > 0) {
        // log all lines but the last one (in case further text is appended later)
        log(startLine, startLine + insertedLines - 1);
        return insertedLines;
    }
    return 0;
}

bool TBuffer::moveCursor(QPoint& where)
{
    const int x = where.x();
    const int y = where.y();
    if (y < 0) {
        return false;
    }
    if (y >= static_cast<int>(buffer.size())) {
        return false;
    }

    if (static_cast<int>(buffer[y].size()) - 1 > x) {
        TChar c(mpConsole);
        // CHECKME: should "buffer[cookedY].size() - 1" be bracketed - which would change the -1 to +1 in the following:
        expandLine(y, x - buffer[y].size() - 1, c);
    }
    return true;
}

// Needed, at least, as a filler for missing lines past end of the lineBuffer
// requested by lua function getLines(...):
QString badLineError = qsl("ERROR: invalid line number");

QString& TBuffer::line(int lineNumber)
{
    if ((lineNumber < 0) || (lineNumber >= lineBuffer.size())) {
        return badLineError;
    }
    return lineBuffer[lineNumber];
}

int TBuffer::find(int line, const QString& what, int pos = 0)
{
    if (lineBuffer[line].size() >= pos) {
        return -1;
    }
    if (pos < 0) {
        return -1;
    }
    if ((line >= static_cast<int>(buffer.size())) || (line < 0)) {
        return -1;
    }
    return lineBuffer[line].indexOf(what, pos);
}

QStringList TBuffer::split(int line, const QString& splitter)
{
    if ((line >= static_cast<int>(buffer.size())) || (line < 0)) {
        return QStringList();
    }
    return lineBuffer[line].split(splitter);
}

QStringList TBuffer::split(int line, const QRegularExpression& splitter)
{
    if ((line >= static_cast<int>(buffer.size())) || (line < 0)) {
        return QStringList();
    }
    return lineBuffer[line].split(splitter);
}

void TBuffer::expandLine(int y, int count, TChar& pC)
{
    const int size = buffer[y].size() - 1;
    for (int i = size, total = size + count; i < total; ++i) {
        buffer[y].push_back(pC);
        lineBuffer[y].append(QChar::Space);
    }
}

bool TBuffer::replaceInLine(QPoint& P_begin, QPoint& P_end, const QString& with, TChar& format)
{
    const int x1 = P_begin.x();
    const int x2 = P_end.x();
    const int y1 = P_begin.y();
    const int y2 = P_end.y();
    if ((y1 >= static_cast<int>(buffer.size())) || (y2 >= static_cast<int>(buffer.size()))) {
        return false;
    }
    if ((x2 > static_cast<int>(buffer[y2].size())) || (x1 > static_cast<int>(buffer[y1].size()))) {
        return false;
    }
    if (x1 < 0 || x2 < 0) {
        return false;
    }

    int xb, xe, yb, ye;
    if (y1 <= y2) {
        yb = y1;
        ye = y2;
        xb = x1;
        xe = x2;
    } else {
        yb = y2;
        ye = y1;
        xb = x2;
        xe = x1;
    }

    for (int y = yb; y <= ye; y++) {
        int x = 0;
        if (y == yb) {
            x = xb;
        }
        int x_end = buffer[y].size() - 1;
        if (y == ye) {
            x_end = xe;
        }
        lineBuffer[y].remove(x, x_end - x);
        auto it1 = buffer[y].begin() + x;
        auto it2 = buffer[y].begin() + x_end;
        buffer[y].erase(it1, it2);
    }

    // insert replacement
    insertInLine(P_begin, with, format);
    return true;
}

void TBuffer::clear()
{
    mCurrentHyperlinkCommand.clear();
    mCurrentHyperlinkHint.clear();
    mCurrentHyperlinkLinkId = 0;
    mHyperlinkActive = false;

    while (!buffer.empty()) {
        if (!deleteLines(0, 0)) {
            break;
        }
    }
    std::deque<TChar> const newLine;
    buffer.push_back(newLine);
    lineBuffer << QString();
    timeBuffer << QString();
    promptBuffer.push_back(false);
}

bool TBuffer::deleteLine(int y)
{
    return deleteLines(y, y);
}

void TBuffer::shrinkBuffer()
{
    for (int i = 0; i < mBatchDeleteSize; ++i) {
        lineBuffer.pop_front();
        promptBuffer.pop_front();
        timeBuffer.pop_front();
        buffer.pop_front();
        mCursorY--;
    }
    // We need to adjust the search result line as some lines have now gone
    // away:
    mpConsole->mCurrentSearchResult = qMax(0, mpConsole->mCurrentSearchResult - mBatchDeleteSize);

    if (mpConsole->getType() & (TConsole::MainConsole|TConsole::UserWindow|TConsole::SubConsole|TConsole::Buffer)) {
        // Signal to lua subsystem that indexes into the Console will need adjusting
        TEvent bufferShrinkEvent{};
        bufferShrinkEvent.mArgumentList.append(QLatin1String("sysBufferShrinkEvent"));
        bufferShrinkEvent.mArgumentTypeList.append(ARGUMENT_TYPE_STRING);
        bufferShrinkEvent.mArgumentList.append(mpConsole->mConsoleName);
        bufferShrinkEvent.mArgumentTypeList.append(ARGUMENT_TYPE_STRING);
        bufferShrinkEvent.mArgumentList.append(QString::number(mBatchDeleteSize));
        bufferShrinkEvent.mArgumentTypeList.append(ARGUMENT_TYPE_NUMBER);
        mpHost->raiseEvent(bufferShrinkEvent);
    }
}

bool TBuffer::deleteLines(int from, int to)
{
    if ((from >= 0) && (from < static_cast<int>(buffer.size())) && (from <= to) && (to >= 0) && (to < static_cast<int>(buffer.size()))) {
        const int delta = to - from + 1;

        for (int i = from, total = from + delta; i < total; ++i) {
            lineBuffer.removeAt(i);
            timeBuffer.removeAt(i);
            promptBuffer.removeAt(i);
        }

        buffer.erase(buffer.begin() + from, buffer.begin() + to + 1);
        return true;
    }
    return false;
}

bool TBuffer::applyLink(const QPoint& P_begin, const QPoint& P_end, const QStringList& linkFunction, const QStringList& linkHint, QVector<int> luaReference)
{
    const int x1 = P_begin.x();
    const int x2 = P_end.x();
    const int y1 = P_begin.y();
    const int y2 = P_end.y();
    int linkID = 0;

    // clang-format off
    if ((x1 >= 0)
        && ((x2 > x1) || (y2 > y1))
        && ((y2 >= 0) && (y2 < static_cast<int>(buffer.size())))
        && (x1 < static_cast<int>(buffer.at(y1).size()))) {
        // clang-format on

        /*
         * Even if the end selection is out of bounds we still apply the format
         * until the end of the line to simplify and ultimately speed up user
         * scripting (no need to calc end of line) - so we don't use:
         * && ( x2 < static_cast<int>(buffer.at(y2).size()) ) )
         */
        for (int y = y1; y <= y2; ++y) {
            int x = 0;
            if (y == y1) {
                x = x1;
            }
            while (x < static_cast<int>(buffer.at(y).size())) {
                if (y >= y2) {
                    if (x >= x2) {
                        return true;
                    }
                }
                if (linkID == 0) {
                    linkID = mLinkStore.addLinks(linkFunction, linkHint, mpHost, luaReference);
                }
                buffer.at(y).at(x++).mLinkIndex = linkID;
            }
        }
        return true;
    }
    return false;
}

// Replaces (bool)TBuffer::applyXxxx(QPoint& P_begin, QPoint& P_end, bool state)
// where Xxxxx is Bold, Italics, Strikeout, Underline
// Can set multiple attributes to given state
bool TBuffer::applyAttribute(const QPoint& P_begin, const QPoint& P_end, const TChar::AttributeFlags attributes, const bool state)
{
    const int x1 = P_begin.x();
    const int x2 = P_end.x();
    const int y1 = P_begin.y();
    const int y2 = P_end.y();

    // clang-format off
    if ((x1 >= 0)
        && ((x2 > x1) || (y2 > y1))
        && ((y2 >= 0) && (y2 < static_cast<int>(buffer.size())))
            && (x1 < static_cast<int>(buffer[y1].size()))) {
        // clang-format on

        /*
         * Even if the end selection is out of bounds we still apply the format
         * until the end of the line to simplify and ultimately speed up user
         * scripting (no need to calc end of line) - so we don't use:
         * && ( x2 < static_cast<int>(buffer.at(y2).size()) ) )
         */

        for (int y = y1; y <= y2; ++y) {
            int x = 0;
            if (y == y1) {
                x = x1;
            }
            while (x < static_cast<int>(buffer.at(y).size())) {
                if (y >= y2) {
                    if (x >= x2) {
                        return true;
                    }
                }
                buffer.at(y).at(x).mFlags = (buffer.at(y).at(x).mFlags &~(attributes)) | (state ? attributes : TChar::None);
                ++x;
            }
        }
        return true;
    }
    return false;
}

bool TBuffer::applyFgColor(const QPoint& P_begin, const QPoint& P_end, const QColor& newColor)
{
    const int x1 = P_begin.x();
    const int x2 = P_end.x();
    const int y1 = P_begin.y();
    const int y2 = P_end.y();

    // clang-format off
    if ((x1 >= 0)
        && ((x2 > x1) || (y2 > y1))
        && ((y2 >= 0) && (y2 < static_cast<int>(buffer.size())))
            && (x1 < static_cast<int>(buffer[y1].size()))) {
        // clang-format on

        /*
         * Even if the end selection is out of bounds we still apply the format
         * until the end of the line to simplify and ultimately speed up user
         * scripting (no need to calc end of line) - so we don't use:
         * && ( x2 < static_cast<int>(buffer.at(y2).size()) ) )
         */

        for (int y = y1; y <= y2; ++y) {
            int x = 0;
            if (y == y1) {
                // Override position start column if on first line to given start column
                x = x1;
            }
            while (x < static_cast<int>(buffer.at(y).size())) {
                if (y >= y2 && x >= x2) {
                    // Escape if on or past last line and past last character on the last line
                    return true;
                }

                buffer.at(y).at(x++).mFgColor = newColor;
            }
        }
        return true;
    }
    return false;
}

bool TBuffer::applyBgColor(const QPoint& P_begin, const QPoint& P_end, const QColor& newColor)
{
    const int x1 = P_begin.x();
    const int x2 = P_end.x();
    const int y1 = P_begin.y();
    const int y2 = P_end.y();

    // clang-format off
    if ((x1 >= 0)
        && ((x2 > x1) || (y2 > y1))
        && ((y2 >= 0) && (y2 < static_cast<int>(buffer.size())))
            && (x1 < static_cast<int>(buffer[y1].size()))) {
        // clang-format on

        /*
         * Even if the end selection is out of bounds we still apply the format
         * until the end of the line to simplify and ultimately speed up user
         * scripting (no need to calc end of line) - so we don't use:
         * && ( x2 < static_cast<int>(buffer.at(y2).size()) ) )
         */

        for (int y = y1; y <= y2; ++y) {
            int x = 0;
            if (y == y1) {
                // Override position start column if on first line to given start column
                x = x1;
            }
            while (x < static_cast<int>(buffer.at(y).size())) {
                if (y >= y2 && x >= x2) {
                    // Escape if on or past last line and on or past last character on last line
                    return true;
                }

                buffer.at(y).at(x++).mBgColor = newColor;
            }
        }
        return true;
    }
    return false;
}

QStringList TBuffer::getEndLines(int n)
{
    QStringList linesList;
    for (int i = getLastLineNumber() - n, total = getLastLineNumber(); i < total; ++i) {
        linesList << line(i);
    }
    return linesList;
}

// This actually only works on a SINGLE line at a time - so was restuctured to
// reflect that in the arguments needed - with sensible defaults on all
// arguments - the positions within the line refer to raw QChar/TChar indexes
// and not graphemes, it is up to the caller to ensure those indexes are useful
// this method only checks that they fit.
// Note: spacePadding is expected to be non-zero on ONLY the first call to this
// method - it is needed to pad the first line out when the first line of a
// selection is not a complete line of text and there are more lines to follow
QString TBuffer::bufferToHtml(const bool showTimeStamp /*= false*/, const int row /*= -1*/,
                              const int endColumn /*= -1*/, const int startColumn /*= 0*/,
                              int spacePadding /*= 0*/)
{
    int pos = startColumn;
    QString s;
    if (row < 0 || row >= static_cast<int>(buffer.size())) {
        // Empty string
        return s;
    }

    // std:deque uses std::deque:size_type as index type which is an unsigned
    // long int, but row (and pos) are signed ints...!
    auto cookedRow = static_cast<unsigned long>(row);

    if ((pos < 0) || (pos >= static_cast<int>(buffer.at(cookedRow).size()))) {
        pos = 0;
    }

    int lastPos = endColumn;
    if (lastPos < 0 || lastPos > static_cast<int>(buffer.at(cookedRow).size())) {
        // lastPos is now at ONE PAST the last valid one to use to index into
        // row - this can have been triggered by a -1 argument
        lastPos = static_cast<int>(buffer.at(cookedRow).size());
    }

    TChar::AttributeFlags currentFlags = TChar::None;
    QColor currentFgColor(Qt::black);
    QColor currentBgColor(Qt::black);
    // This combination of color values (black on black) cannot usefully be used in practice
    // - so use as initialization values

    // Assume we are on the first line until told otherwise - and we will need
    // to NOT close a previous <span ...>:
    bool firstSpan = true;
    // If times stamps are to be shown AND the first line is a partial
    // then we need:
    // <span timestamp format>Timestamp (13 chars)</span><span default>___padding spaces___</span><span first chunk style>first chunk...
    // we will NOT need a closing "</span>"
    if (showTimeStamp && !timeBuffer.at(row).isEmpty()) {
        // TODO: formatting according to TTextEdit.cpp: if( i2 < timeOffset ) - needs updating if we allow the colours to be user set:
        s.append(qsl("<span style=\"color: rgb(200,150,0); background: rgb(22,22,22); \">%1").arg(timeBuffer.at(row).left(mudlet::smTimeStampFormat.length())));
        // Set the current idea of what the formatting is so we can spot if it
        // changes:
        currentFgColor = QColor(200, 150, 0);
        currentBgColor = QColor(22, 22, 22);
        currentFlags = TChar::None;
        // We are no longer before the first span - so we need to flag that
        // there will be one to close:
        firstSpan = false;
    }

    if (spacePadding > 0) {
        // used for "copy HTML", this is the first line of selection (because of
        // the padding needed)
        if (firstSpan) {
            // Must skip the close of the preceding span as there isn't one
            firstSpan = false;
        } else {
            s.append(QLatin1String("</span>"));
        }

        // Pad out with spaces to the right so a partial first line lines up
        s.append(qsl("<span>%1").arg(QString(spacePadding, QChar::Space)));
    }

    for (auto cookedPos = static_cast<unsigned long>(pos); pos < lastPos; ++cookedPos, ++pos) {
        // Do we need to start a new span?
        if (firstSpan
            || buffer.at(cookedRow).at(cookedPos).mFgColor != currentFgColor
            || buffer.at(cookedRow).at(cookedPos).mBgColor != currentBgColor
            || (buffer.at(cookedRow).at(cookedPos).mFlags & TChar::TestMask) != currentFlags) {

            if (firstSpan) {
                firstSpan = false; // The first span - won't need to close the previous one
            } else {
                s.append(QLatin1String("</span>"));
            }
            currentFgColor = buffer.at(cookedRow).at(cookedPos).mFgColor;
            currentBgColor = buffer.at(cookedRow).at(cookedPos).mBgColor;
            currentFlags = buffer.at(cookedRow).at(cookedPos).mFlags & TChar::TestMask;

            // clang-format off
            if (currentFlags & TChar::Reverse) {
                // Swap the fore and background colours:
                s.append(qsl("<span style=\"color: rgb(%1,%2,%3); background: rgb(%4,%5,%6); %7%8%9\">")
                         .arg(QString::number(currentBgColor.red()), QString::number(currentBgColor.green()), QString::number(currentBgColor.blue()), // args 1 to 3
                              QString::number(currentFgColor.red()), QString::number(currentFgColor.green()), QString::number(currentFgColor.blue()), // args 4 to 6
                              currentFlags & TChar::Bold ? QLatin1String(" font-weight: bold;") : QString(), // arg 7
                              currentFlags & TChar::Italic ? QLatin1String(" font-style: italic;") : QString(), // arg 8
                              currentFlags & (TChar::Underline | TChar::StrikeOut | TChar::Overline ) // remainder is arg 9
                              ? qsl(" text-decoration:%1%2%3")
                                .arg(currentFlags & TChar::Underline ? QLatin1String(" underline") : QString(),
                                     currentFlags & TChar::StrikeOut ? QLatin1String(" line-through") : QString(),
                                     currentFlags & TChar::Overline ? QLatin1String(" overline") : QString())
                              : QString()));
            } else {
                s.append(qsl("<span style=\"color: rgb(%1,%2,%3); background: rgb(%4,%5,%6); %7%8%9\">")
                         .arg(QString::number(currentFgColor.red()), QString::number(currentFgColor.green()), QString::number(currentFgColor.blue()), // args 1 to 3
                              QString::number(currentBgColor.red()), QString::number(currentBgColor.green()), QString::number(currentBgColor.blue()), // args 4 to 6
                              currentFlags & TChar::Bold ? QLatin1String(" font-weight: bold;") : QString(), // arg 7
                              currentFlags & TChar::Italic ? QLatin1String(" font-style: italic;") : QString(), // arg 8
                              currentFlags & (TChar::Underline | TChar::StrikeOut | TChar::Overline ) // remainder is arg 9
                              ? qsl(" text-decoration:%1%2%3")
                                .arg(currentFlags & TChar::Underline ? QLatin1String(" underline") : QString(),
                                     currentFlags & TChar::StrikeOut ? QLatin1String(" line-through") : QString(),
                                     currentFlags & TChar::Overline ? QLatin1String(" overline") : QString())
                              : QString()));
            }
            // clang-format on
        }
        if (lineBuffer.at(row).at(pos) == QChar('<')) {
            s.append(QLatin1String("&lt;"));
        } else if (lineBuffer.at(row).at(pos) == QChar('>')) {
            s.append("&gt;");
        } else {
            s.append(lineBuffer.at(row).at(pos));
        }
    }
    if (!s.isEmpty()) {
        s.append(QLatin1String("</span>"));
        // Needed to balance the very first open <span>, but only if we have
        // included anything. the previously appearing <br /> is an XML tag, NOT
        // a (strict) HTML 4 one
    }

    s.append(QLatin1String("<br>\n"));
    // Needed to reproduce empty lines in capture, as this method is called for
    // EACH line, even the empty ones, the spans are styled as "pre" so literal
    // linefeeds would be treated as such THERE but we deliberately place the
    // line-feeds OUTSIDE so they come under the <body>s no wrap and as such
    // line-feeds can be used to break the HTML over lots of lines (which is
    // easier to hand edit and examine afterwards) without impacting the
    // formatting. To get the line feeds at the end of displayed HTML lines the
    // <br> is used.  This slightly weird way of doing things is so that some
    // on-line tools preserve the formatting when the HTML-lised selection is
    // pasted to them AND retain the ability to paste the HTML from the
    // clipboard into a plain text editor and not have everything on one line in
    // that editor!

    return s;
}

bool TBuffer::processUtf8Sequence(const std::string& bufferData, const bool isFromServer, const size_t len, size_t& pos, bool& isNonBMPCharacter)
{
    // In Utf-8 mode we have to process the data more than one byte at a
    // time because there is not necessarily a one-byte to one TChar
    // mapping, instead we use one TChar per QChar - and that has to be
    // tweaked for non-BMP characters that use TWO QChars per codepoint.
    if (bufferData.at(pos) & 0x80) {
        // MSB is set, so if this is Utf-8 then assume this is the first byte
        size_t utf8SequenceLength = 1;
        if ((bufferData.at(pos) & 0xE0) == 0xC0) {
            // 2 byte sequence - Unicode code-points: U+00000080 to U+000007FF
            utf8SequenceLength = 2;
        } else if ((bufferData.at(pos) & 0xF0) == 0xE0) {
            // 3 byte sequence - Unicode code-points: U+00000800 to U+0000FFFF
            utf8SequenceLength = 3;
        } else if ((bufferData.at(pos) & 0xF8) == 0xF0) {
            // 4 byte sequence - Unicode code-points: U+00010000 to U+001FFFFF (<= U+0010FFF LEGAL)
            utf8SequenceLength = 4;
        } else if ((bufferData.at(pos) & 0xFC) == 0xF8) {
            // 5 byte sequence - Unicode code-points: U+00200000 to U+03FFFFFF (ALL ILLEGAL)
            utf8SequenceLength = 5;
        } else if ((bufferData.at(pos) & 0xFE) == 0xFC) {
            // 6 byte sequence - Unicode code-points: U+04000000 to U+7FFFFFFF (ALL ILLEGAL)
            utf8SequenceLength = 6;
        }

        if ((pos + utf8SequenceLength) > len) {
            // Not enough bytes left in bufferData to complete the utf-8
            // sequence - need to save and prepend onto incoming data next
            // time around.
            // The absence of a second argument takes all the available
            // bytes - this is only for data from the Server NOT from
            // locally generated material from Lua feedTriggers(...)
            if (isFromServer) {
#if defined(DEBUG_UTF8_PROCESSING)
                qDebug() << "TBuffer::processUtf8Sequence(...) Insufficient bytes in buffer to complete UTF-8 sequence, need:" << utf8SequenceLength
                         << " but we currently only have: " << bufferData.substr(pos).length() << " bytes (which we will store for next call to this method)...";
#endif
                mIncompleteSequenceBytes = bufferData.substr(pos);
            }
            return false; // Bail out
        }

        // If we have got here we have enough bytes to work with:
        bool isValid = true;
        bool isToUseReplacementMark = false;
        bool isToUseByteOrderMark = false; // When BOM seen in stream it transcodes as zero characters
        switch (utf8SequenceLength) {
        case 4:
            // Check the 4th byte is a valid continuation byte (2 MS-Bits are 10)
            if ((bufferData.at(pos + 3) & 0xC0) != 0x80) {
#if defined(DEBUG_UTF8_PROCESSING)
                qDebug() << "TBuffer::processUtf8Sequence(...) 4th byte in UTF-8 sequence is invalid!";
#endif
                isValid = false;
                isToUseReplacementMark = true;
            } else if (((bufferData.at(pos) & 0x07) > 0x04) || (((bufferData.at(pos) & 0x07) == 0x04) && ((bufferData.at(pos + 1) & 0x3F) > 0x0F))) {
// For 4 byte values the bits are distributed:
//  Byte 1    Byte 2    Byte 3    Byte 4
// 11110ABC  10DEFGHI  10JKLMNO  10PQRSTU   A is MSB
// U+10FFFF in binary is: 1 0000 1111 1111 1111 1111
// So this (the maximum valid character) is:
//      ABC    DEFGHI    JKLMNO    PQRSTU
//      100    001111    111111    111111
// So if the first byte bufferData.at(pos] & 0x07 is:
//  < 0x04 then must be in range
//  > 0x04 then must be out of range
// == 0x04 then consider bufferData.at(pos+1] & 0x3F:
//     <= 001111 0x0F then must be in range
//      > 001111 0x0F then must be out of range

#if defined(DEBUG_UTF8_PROCESSING)
                qDebug() << "TBuffer::processUtf8Sequence(...) 4 byte UTF-8 sequence is valid but is beyond range of legal codepoints!";
#endif
                isValid = false;
                isToUseReplacementMark = true;
            }

        // Fall-through
            [[fallthrough]];
        case 3:
            // Check the 3rd byte is a valid continuation byte (2 MS-Bits are 10)
            if ((bufferData.at(pos + 2) & 0xC0) != 0x80) {
#if defined(DEBUG_UTF8_PROCESSING)
                qDebug() << "TBuffer::processUtf8Sequence(...) 3rd byte in UTF-8 sequence is invalid!";
#endif
                isValid = false;
                isToUseReplacementMark = true;
            } else if ((bufferData.at(pos) & 0x0F) == 0x0D && (bufferData.at(pos + 1) & 0x20) == 0x20) {
// For 3 byte values the bits are distributed:
//  Byte 1    Byte 2    Byte 3
// 1110ABCD  10DEFGHI  10JKLMNO   A is MSB
// First High surrogate 0xed 0xa0 0x80 (U+D800)
// 1101 1000 0000 0000
// ----1101  --100000  --000000
// Last Low surrogate 0xed 0xbf 0xbf (U+DFFF)
// 1101 1111 1111 1111
// ----1101  --111111  --111111
/*
    * As per Wikipedia {https://en.wikipedia.org/wiki/UTF-16#U.2BD800_to_U.2BDFFF}
    * "The Unicode standard permanently reserves these code point values for UTF-16
    * encoding of the high and low surrogates, and they will never be assigned a
    * character, so there should be no reason to encode them. The official Unicode
    * standard says that no UTF forms, including UTF-16, can encode these code
    * points.
    *
    * However UCS-2, UTF-8, and UTF-32 can encode these code points in trivial and
    * obvious ways, and large amounts of software does so even though the standard
    * states that such arrangements should be treated as encoding errors. It is
    * possible to unambiguously encode them in UTF-16 by using a code unit equal to
    * the code point, as long as no sequence of two code units can be interpreted
    * as a legal surrogate pair (that is, as long as a high surrogate is never
    * followed by a low surrogate). The majority of UTF-16 encoder and decoder
    * implementations translate between encodings as though this were the case
    * and Windows allows such sequences in filenames."
    *
    * So test for and, considering the LS Nibble of first byte:
    * * accept if LS Nibble of first byte is             less than 0xD
    * * accept if LS Nibble of first byte is greater than/equal to 0xE
    * * otherwise (if LS Nibble of first byte IS 0xD)
    *   * accept if 6 LS Bits of second byte is 0x1F of or less
    * Conversely this can be stated as:
    * * reject if LS Nibble of first byte is 0xD AND 6th MS Bit of second byte is set
    */
#if defined(DEBUG_UTF8_PROCESSING)
                qDebug() << "TBuffer::processUtf8Sequence(...) 3 byte UTF-8 sequence is a High or Low UTF-16 Surrogate and is not valid in UTF-8!";
#endif
                isValid = false;
                isToUseReplacementMark = true;
            } else if (   (static_cast<quint8>(bufferData.at(pos + 2)) == 0xBF)
                       && (static_cast<quint8>(bufferData.at(pos + 1)) == 0xBB)
                       && (static_cast<quint8>(bufferData.at(pos    )) == 0xEF)) {

                // Got caught out by this one - it is the UTF-8 BOM (or
                // Zero-Width No-Break Space) and needs to be detected specially
                // as Qt's codec ignores it and transcodes it to NO codepoints!
#if defined(DEBUG_UTF8_PROCESSING)
                qDebug() << "TBuffer::processUtf8Sequence(...) UTF-8 BOM sequence seen and handled!";
#endif
                isValid = false;
                isToUseByteOrderMark = true;
            }

        // Fall-through
            [[fallthrough]];
        case 2:
            // Check the 2nd byte is a valid continuation byte (2 MS-Bits are 10)
            if ((static_cast<quint8>(bufferData.at(pos + 1)) & 0xC0) != 0x80) {
#if defined(DEBUG_UTF8_PROCESSING)
                qDebug() << "TBuffer::processUtf8Sequence(...) 2nd byte in UTF-8 sequence is invalid!";
#endif
                isValid = false;
                isToUseReplacementMark = true;
            }

            // clang-format off
            // Disable code reformatting as it would destroy layout that helps
            // to explain the grouping of the tests
            // Also test for (and reject) overlong sequences - don't
            // need to check 5 or 6 ones as those are already rejected:
            if (  ( ((static_cast<quint8>(bufferData.at(pos    )) & 0xFE) == 0xC0) && ( ( static_cast<quint8>(bufferData.at(pos + 1)) & 0xC0) == 0x80) )
                ||( ( static_cast<quint8>(bufferData.at(pos    )        ) == 0xE0) && ( ( static_cast<quint8>(bufferData.at(pos + 1)) & 0xE0) == 0x80) )
                ||( ( static_cast<quint8>(bufferData.at(pos    )        ) == 0xF0) && ( ( static_cast<quint8>(bufferData.at(pos + 1)) & 0xF0) == 0x80) ) ) {
// clang-format on

#if defined(DEBUG_UTF8_PROCESSING)
                qDebug().nospace() << "TBuffer::processUtf8Sequence(...) Overlong " << utf8SequenceLength << "-byte sequence as UTF-8 rejected!";
#endif
                isValid = false;
                isToUseReplacementMark = true;
            }
            break;

        default:
#if defined(DEBUG_UTF8_PROCESSING)
            qDebug().nospace() << "TBuffer::processUtf8Sequence(...) " << utf8SequenceLength << "-byte sequence as UTF-8 rejected!";
#endif
            isValid = false;
            isToUseReplacementMark = true;
        }

        // Will be one (BMP codepoint) or two (non-BMP codepoints) QChar(s)
        if (isValid) {
            const QString codePoint = QString(bufferData.substr(pos, utf8SequenceLength).c_str());
            switch (codePoint.size()) {
            default:
                Q_UNREACHABLE(); // This can't happen, unless we got start or length wrong in std::string::substr()
                qWarning().nospace() << "TBuffer::processUtf8Sequence(...) " << utf8SequenceLength << "-byte UTF-8 sequence accepted, and it encoded to " << codePoint.size()
                                     << " QChars which does not make sense!!!";
                isValid = false;
                isToUseReplacementMark = true;
                break;
            case 2:
                isNonBMPCharacter = true;
                // Fall-through
                [[fallthrough]];
            case 1:
#if defined(DEBUG_UTF8_PROCESSING)
                qDebug().nospace() << "TBuffer::processUtf8Sequence(...) " << utf8SequenceLength << "-byte UTF-8 sequence accepted, it was " << codePoint.size() << " QChar(s) long [" << codePoint
                                   << "]";
#endif
                mMudLine.append(codePoint);
                break;
            case 0:
                qWarning().nospace() << "TBuffer::processUtf8Sequence(...) " << utf8SequenceLength << "-byte UTF-8 sequence accepted, but it did not encode to "
                                                                                                      "ANY QChar(s)!!!";
                isValid = false;
                isToUseReplacementMark = true;
            }
        }

        if (!isValid) {
#if defined(DEBUG_UTF8_PROCESSING)
            QString debugMsg;
            for (size_t i = 0; i < utf8SequenceLength; ++i) {
                debugMsg.append(qsl("<%1>").arg(static_cast<quint8>(bufferData.at(pos + i)), 2, 16, QChar('0')));
            }
            qDebug().nospace() << "    Sequence bytes are: " << debugMsg;
#endif
            if (isToUseReplacementMark) {
                mMudLine.append(QChar::ReplacementCharacter);
            } else if (isToUseByteOrderMark) {
                mMudLine.append(QChar::ByteOrderMark);
            }
        }

        // As there is already a unit increment at the bottom of loop
        // add one less than the sequence length:
        pos += utf8SequenceLength - 1;
    } else {
        // Single byte character i.e. Unicode points: U+00000000 to U+0000007F
        mMudLine.append(bufferData.at(pos));
    }

    return true;
}

bool TBuffer::processGBSequence(const std::string& bufferData, const bool isFromServer, const bool isGB18030, const size_t len, size_t& pos, bool& isNonBmpCharacter)
{
// In GBK/GB18030 mode we have to process the data more than one byte at a
// time because there is not necessarily a one-byte to one TChar
// mapping, instead we use one TChar per QChar - and that has to be
// tweaked for non-BMP characters that use TWO QChars per codepoint.
// GB2312 is the predecessor to both and - according to Wikipedia (EN) covers
// over 99% of the characters of contemporary usage.
// GBK is a sub-set of GB18030 so can be processed in the same method
// Assume we are at the first byte of a single (ASCII), pair (GBK/GB18030)
// or four byte (GB18030) sequence

#if defined(DEBUG_GB_PROCESSING)
    std::string dataIdentity;
#endif

    // The range deductions for two-byte sequences are take from:
    // https://en.wikipedia.org/wiki/GBK#Encoding
    size_t gbSequenceLength = 1;
    bool isValid = true;
    bool isToUseReplacementMark = false;
    // Only set this if we are adding more than one code-point to
    // mCurrentLineCharacters:
    isNonBmpCharacter = false;
    if (static_cast<quint8>(bufferData.at(pos)) < 0x80) {
        // Is ASCII - single byte character, straight forward for a "first" byte case
        mMudLine.append(bufferData.at(pos));
        // As there is already a unit increment at the bottom of caller's loop
        // there is no need to tweak pos in THIS case

        return true;
    } else if (static_cast<quint8>(bufferData.at(pos)) == 0x80) {
        // Invalid as first byte
        isValid = false;
        isToUseReplacementMark = true;
#if defined(DEBUG_GB_PROCESSING)
        qDebug().nospace() << "TBuffer::processGBSequence(...) 1-byte sequence as " << (isGB18030 ? "GB18030" : "GB2312/GBK") << " rejected!";
#endif

        // Proceed to handle 1 byte (as GB2312/GBK/GB18030 data) outside of checks...

    } else if (!isGB18030) {
        // Could be two byte GBK - but do we have a second byte?
        // As we are not in GB18030 mode treat it as if it is a 2 byte sequence
        gbSequenceLength = 2;
        if ((pos + gbSequenceLength - 1) < len) {
            // We have enough bytes to look at the second one - let's see which
            // range it is in:
            // clang-format off
            if        (  (static_cast<quint8>(bufferData.at(pos    )) >= 0x81) && (static_cast<quint8>(bufferData.at(pos    )) <= 0xA0)
                      && (static_cast<quint8>(bufferData.at(pos + 1)) >= 0x40) && (static_cast<quint8>(bufferData.at(pos + 1)) <= 0xFE)
                      && (static_cast<quint8>(bufferData.at(pos + 1)) != 0x7F) ) {
// clang-format on
// GBK Area 3 sequence

#if defined(DEBUG_GB_PROCESSING)
                dataIdentity = "GBK Area 3";
#endif

                // clang-format off
            } else if (  (static_cast<quint8>(bufferData.at(pos    )) >= 0xA1) && (static_cast<quint8>(bufferData.at(pos    )) <= 0xA9)
                      && (static_cast<quint8>(bufferData.at(pos + 1)) >= 0xA1) && (static_cast<quint8>(bufferData.at(pos + 1)) <= 0xFE) ) {
// clang-format on
// GBK Area 1 (& GB2312) sequence

#if defined(DEBUG_GB_PROCESSING)
                dataIdentity = "GBK Area 1 (or GB2312)";
#endif

                // clang-format off
            } else if (  (static_cast<quint8>(bufferData.at(pos    )) >= 0xB0) && (static_cast<quint8>(bufferData.at(pos    )) <= 0xF7)
                      && (static_cast<quint8>(bufferData.at(pos + 1)) >= 0xA1) && (static_cast<quint8>(bufferData.at(pos + 1)) <= 0xFE) ) {
// clang-format on
// GBK Area 2 (& GB2312) sequence

#if defined(DEBUG_GB_PROCESSING)
                dataIdentity = "GBK Area 2 (or GB2312)";
#endif

                // clang-format off
            } else if (  (static_cast<quint8>(bufferData.at(pos    )) >= 0xA8) && (static_cast<quint8>(bufferData.at(pos    )) <= 0xA9)
                      && (static_cast<quint8>(bufferData.at(pos + 1)) >= 0x40) && (static_cast<quint8>(bufferData.at(pos + 1)) <= 0xA0)
                      && (static_cast<quint8>(bufferData.at(pos + 1)) != 0x7F) ) {
// clang-format on
// GBK/5 sequence

#if defined(DEBUG_GB_PROCESSING)
                dataIdentity = "GBK Area 5";
#endif

                // clang-format off
            } else if (  (static_cast<quint8>(bufferData.at(pos    )) >= 0xAA) && (static_cast<quint8>(bufferData.at(pos    )) <= 0xFE)
                      && (static_cast<quint8>(bufferData.at(pos + 1)) >= 0x40) && (static_cast<quint8>(bufferData.at(pos + 1)) <= 0xA0)
                      && (static_cast<quint8>(bufferData.at(pos + 1)) != 0x7F) ) {
// clang-format on
// GBK/4 sequence

#if defined(DEBUG_GB_PROCESSING)
                dataIdentity = "GBK Area 4";
#endif

                // clang-format off
            } else if (  (static_cast<quint8>(bufferData.at(pos    )) >= 0xAA) && (static_cast<quint8>(bufferData.at(pos    )) <= 0xAF)
                      && (static_cast<quint8>(bufferData.at(pos + 1)) >= 0xA1) && (static_cast<quint8>(bufferData.at(pos + 1)) <= 0xFE) ) {
// clang-format on
// User Defined 1 sequence - possibly invalid for us but if the
// MUD supplies their own font it could be used:

#if defined(DEBUG_GB_PROCESSING)
                dataIdentity = "User Defined (PU) Area 1";
#endif

                // clang-format off
            } else if (  (static_cast<quint8>(bufferData.at(pos    )) >= 0xF8) && (static_cast<quint8>(bufferData.at(pos    )) <= 0xFE)
                      && (static_cast<quint8>(bufferData.at(pos + 1)) >= 0xA1) && (static_cast<quint8>(bufferData.at(pos + 1)) <= 0xFE) ) {
// clang-format on
// User Defined 2 sequence - possibly invalid for us but if the
// MUD supplies their own font it could be used:

#if defined(DEBUG_GB_PROCESSING)
                dataIdentity = "User Defined (PU) Area 2";
#endif

                // clang-format off
            } else if (  (static_cast<quint8>(bufferData.at(pos    )) >= 0xA1) && (static_cast<quint8>(bufferData.at(pos    )) <= 0xA7)
                      && (static_cast<quint8>(bufferData.at(pos + 1)) >= 0xA1) && (static_cast<quint8>(bufferData.at(pos + 1)) <= 0xFE)
                      && (static_cast<quint8>(bufferData.at(pos + 1)) != 0x7F) ) {
// clang-format on
// User Defined 3 sequence - possibly invalid for us but if the
// MUD supplies their own font it could be used:

#if defined(DEBUG_GB_PROCESSING)
                dataIdentity = "User Defined (PU) Area 3";
#endif

                // clang-format off
            } else if (  (static_cast<quint8>(bufferData.at(pos    )) >= 0x90) && (static_cast<quint8>(bufferData.at(pos    )) <= 0xE3)
                      && (static_cast<quint8>(bufferData.at(pos + 1)) >= 0x30) && (static_cast<quint8>(bufferData.at(pos + 1)) <= 0x39) ) {
// clang-format on
// First two bytes of a 4-byte GB18030 sequence - for a non-BMP mapped Unicode
// codepoint if byte 3 is within 0x81-00xFE and byte 4 is within 0x30-0x39
                isValid = false;
                isToUseReplacementMark = true;
#if defined(DEBUG_GB_PROCESSING)
                qDebug().nospace() << "TBuffer::processGBSequence(...) 2-byte sequence as "
                                      "GB2312/GBK rejected because it is seems to be the "
                                      "first pair of a 4-byte GB18030 non-BMP Unicode sequence!";
#endif
                // clang-format off
            } else if (  (static_cast<quint8>(bufferData.at(pos    )) >= 0xFD) && (static_cast<quint8>(bufferData.at(pos    )) <= 0xFE)
                      && (static_cast<quint8>(bufferData.at(pos + 1)) >= 0x30) && (static_cast<quint8>(bufferData.at(pos + 1)) <= 0x39) ) {
// clang-format on
// First two bytes of a 4-byte GB18030 sequence - for a non-BMP mapped Unicode
// codepoint if byte 3 is within 0x81-00xFE and byte 4 is within 0x30-0x39
                isValid = false;
                isToUseReplacementMark = true;
#if defined(DEBUG_GB_PROCESSING)
                qDebug().nospace() << "TBuffer::processGBSequence(...) 2-byte sequence as "
                                      "GB2312/GBK rejected because it is seems to be the "
                                      "first pair of a 4-byte GB18030 Private Use sequence!";
#endif
            } else {
                // Outside expected ranges

                isValid = false;
                isToUseReplacementMark = true;
#if defined(DEBUG_GB_PROCESSING)
                qDebug().nospace() << "TBuffer::processGBSequence(...) 2-byte sequence as "
                                      "GB2312/GBK rejected!";
#endif
            }

            // Proceed to handle 2 bytes (of GB2312/GBK data) outside of checks...

        } else {
            // Not enough bytes to process yet - so store what we have and return
            if (isFromServer) {
#if defined(DEBUG_GB_PROCESSING)
                qDebug().nospace() << "TBuffer::processGBSequence(...) Insufficient bytes in buffer to "
                                      "complete GB2312/GBK sequence, need at least: "
                                   << gbSequenceLength << " but we currently only have: " << bufferData.substr(pos).length() << " bytes (which we will store for next call to this method)...";
#endif
                mIncompleteSequenceBytes = bufferData.substr(pos);
            }
            return false; // Bail out
        }

    } else {
        // isGB18030 is true!
        // Could be two bytes or four bytes - but do we have at least a second
        // byte? Treat it as if it is a 2 byte sequence until we know we have a
        // four byte one - from examining the second byte and it is in range
        // 0x30 to 0x39 inclusive:

        gbSequenceLength = 2;
        if ((pos + gbSequenceLength - 1) < len) {
            // We have enough bytes to look at the second one - let's see which
            // range it is in:
            // clang-format off
            if (  (static_cast<quint8>(bufferData.at(pos    )) >= 0x81) && (static_cast<quint8>(bufferData.at(pos    )) <= 0xFE)
               && (static_cast<quint8>(bufferData.at(pos + 1)) >= 0x30) && (static_cast<quint8>(bufferData.at(pos + 1)) <= 0x39) ) {
                // clang-format on
                // This IS a 4-byte sequence

                gbSequenceLength = 4;

                if ((pos + gbSequenceLength - 1) >= len) {
                    // Not enough bytes to process yet - so store what we have and return
                    if (isFromServer) {
#if defined(DEBUG_GB_PROCESSING)
                        qDebug().nospace() << "TBuffer::processGBSequence(...) Insufficient bytes in buffer to "
                                              "complete GB18030 sequence, need at least: "
                                           << gbSequenceLength << " but we currently only have: " << bufferData.substr(pos).length() << " bytes (which we will store for next call to this method)...";
#endif
                        mIncompleteSequenceBytes = bufferData.substr(pos);
                    }

                    return false; // Bail out
                }

                // clang-format off
                // Continue with four-byte sequence validation processing as we
                // have all four bytes to work with:
                if (   ((  /* 1st group low limit 0x81 is already done*/        (static_cast<quint8>(bufferData.at(pos    )) <= 0x84))
                        ||((static_cast<quint8>(bufferData.at(pos)) >= 0x90) && (static_cast<quint8>(bufferData.at(pos    )) <= 0xE3)))
                    /*
                     * Only the above 1st byte ranges are currently used - others are reserved
                     * Second byte range is 0x30-0x39 for all and has already been checked
                     */
                    && (static_cast<quint8>(bufferData.at(pos + 2)) >= 0x81) && (static_cast<quint8>(bufferData.at(pos + 2)) <= 0xFE)
                    && (static_cast<quint8>(bufferData.at(pos + 3)) >= 0x30) && (static_cast<quint8>(bufferData.at(pos + 3)) <= 0x39) ) {

                    // Okay we should have a valid four byte sequence now
#if defined(DEBUG_GB_PROCESSING)
                    dataIdentity = "Non-BMP mapped Unicode";
#endif
                } else {
                    // if first byte was < 0x90 then it would have been a BMP
                    // unicode codepoint but it is academic as it is not
                    // currently defined as a valid codepoint value and will be
                    // substituted with the replacement character anyway:
                    // clang-format on

                    isValid = false;
                    isToUseReplacementMark = true;

#if defined(DEBUG_GB_PROCESSING)
                    qDebug().nospace() << "TBuffer::processGBSequence(...) 4-byte sequence as "
                                          "GB18030 rejected!";
#endif
                }

                // Proceed to handle 4 bytes (as GB18030 data) outside of checks...

            } else {
                // Looks as though it is a two-byte sequence after all - so
                // validate it as that:
                // clang-format off
                if        (  (static_cast<quint8>(bufferData.at(pos    )) >= 0x81) && (static_cast<quint8>(bufferData.at(pos    )) <= 0xA0)
                          && (static_cast<quint8>(bufferData.at(pos + 1)) >= 0x40) && (static_cast<quint8>(bufferData.at(pos + 1)) <= 0xFE)
                          && (static_cast<quint8>(bufferData.at(pos + 1)) != 0x7F) ) {
                    // clang-format on
                    // GBK/3 sequence

#if defined(DEBUG_GB_PROCESSING)
                    dataIdentity = "GBK Area 3";
#endif

                // clang-format off
                } else if (  (static_cast<quint8>(bufferData.at(pos    )) >= 0xA1) && (static_cast<quint8>(bufferData.at(pos    )) <= 0xA9)
                          && (static_cast<quint8>(bufferData.at(pos + 1)) >= 0xA1) && (static_cast<quint8>(bufferData.at(pos + 1)) <= 0xFE) ) {
                    // clang-format on
                    // GBK/1 (& GB2312) sequence

#if defined(DEBUG_GB_PROCESSING)
                    dataIdentity = "GBK Area 1";
#endif

                // clang-format off
                } else if (  (static_cast<quint8>(bufferData.at(pos    )) >= 0xB0) && (static_cast<quint8>(bufferData.at(pos    )) <= 0xF7)
                          && (static_cast<quint8>(bufferData.at(pos + 1)) >= 0xA1) && (static_cast<quint8>(bufferData.at(pos + 1)) <= 0xFE) ) {
                    // clang-format on
                    // GBK/2 (& GB2312) sequence

#if defined(DEBUG_GB_PROCESSING)
                    dataIdentity = "GBK Area 2";
#endif

                // clang-format off
                } else if (  (static_cast<quint8>(bufferData.at(pos    )) >= 0xA8) && (static_cast<quint8>(bufferData.at(pos    )) <= 0xA9)
                          && (static_cast<quint8>(bufferData.at(pos + 1)) >= 0x40) && (static_cast<quint8>(bufferData.at(pos + 1)) <= 0xA0)
                          && (static_cast<quint8>(bufferData.at(pos + 1)) != 0x7F) ) {
                    // clang-format on
                    // GBK/5 sequence

#if defined(DEBUG_GB_PROCESSING)
                    dataIdentity = "GBK Area 5";
#endif

                // clang-format off
                } else if (  (static_cast<quint8>(bufferData.at(pos    )) >= 0xAA) && (static_cast<quint8>(bufferData.at(pos    )) <= 0xFE)
                          && (static_cast<quint8>(bufferData.at(pos + 1)) >= 0x40) && (static_cast<quint8>(bufferData.at(pos + 1)) <= 0xA0)
                          && (static_cast<quint8>(bufferData.at(pos + 1)) != 0x7F) ) {
                    // clang-format on
                    // GBK/4 sequence

#if defined(DEBUG_GB_PROCESSING)
                    dataIdentity = "GBK Area 4";
#endif

                // clang-format off
                } else if (  (static_cast<quint8>(bufferData.at(pos    )) >= 0xAA) && (static_cast<quint8>(bufferData.at(pos    )) <= 0xAF)
                          && (static_cast<quint8>(bufferData.at(pos + 1)) >= 0xA1) && (static_cast<quint8>(bufferData.at(pos + 1)) <= 0xFE) ) {
                    // clang-format on
                    // User Defined 1 sequence - possibly invalid for us but if the
                    // MUD supplies their own font it could be used:

#if defined(DEBUG_GB_PROCESSING)
                    dataIdentity = "User Defined (PU) Area 1";
#endif

                // clang-format off
                } else if (  (static_cast<quint8>(bufferData.at(pos    )) >= 0xF8) && (static_cast<quint8>(bufferData.at(pos    )) <= 0xFE)
                          && (static_cast<quint8>(bufferData.at(pos + 1)) >= 0xA1) && (static_cast<quint8>(bufferData.at(pos + 1)) <= 0xFE) ) {
                    // clang-format on
                    // User Defined 2 sequence - possibly invalid for us but if the
                    // MUD supplies their own font it could be used:

#if defined(DEBUG_GB_PROCESSING)
                    dataIdentity = "User Defined (PU) Area 2";
#endif

                // clang-format off
                } else if (  (static_cast<quint8>(bufferData.at(pos    )) >= 0xA1) && (static_cast<quint8>(bufferData.at(pos    )) <= 0xA7)
                          && (static_cast<quint8>(bufferData.at(pos + 1)) >= 0xA1) && (static_cast<quint8>(bufferData.at(pos + 1)) <= 0xFE)
                          && (static_cast<quint8>(bufferData.at(pos + 1)) != 0x7F) ) {
                    // clang-format on
                    // User Defined 3 sequence - possibly invalid for us but if the
                    // MUD supplies their own font it could be used:

#if defined(DEBUG_GB_PROCESSING)
                    dataIdentity = "User Defined (PU) Area 3";
#endif

                } else {
                    // Outside expected range

                    isValid = false;
                    isToUseReplacementMark = true;
#if defined(DEBUG_GB_PROCESSING)
                    qDebug().nospace() << "TBuffer::processGBSequence(...) 2-byte sequence as GB18030 rejected!";
#endif
                }

            } // End of IF is a four-byte ELSE is a two-byte sequence...

            // Proceed to handle 2 bytes (of GB18030 data) outside of checks...

        } else {
            // Not enough bytes to process yet could be we need two OR four but
            // we only have one - so store what we have and return
            if (isFromServer) {
#if defined(DEBUG_GB_PROCESSING)
                qDebug().nospace() << "TBuffer::processGBSequence(...) Insufficient bytes in buffer to complete GB18030 sequence, need at least:"
                                   << gbSequenceLength << " but we currently only have: " << bufferData.substr(pos).length()
                                   << " bytes (which we will store for next call to this method)...";
#endif
                mIncompleteSequenceBytes = bufferData.substr(pos);
            }
            return false; // Bail out
        }
    }

    // At this point we know how many bytes to consume, and whether they are in
    // the right ranges of individual values to be valid

    if (isValid) {
        // Try and convert two or four byte sequence to Unicode using Qts own
        // decoder - and check number of codepoints returned

        QString codePoint;
        if (TEncodingHelper::isEncodingAvailable(mEncoding)) {
            codePoint = TEncodingHelper::decode(QByteArray::fromRawData(bufferData.substr(pos, gbSequenceLength).c_str(), gbSequenceLength), mEncoding);
            switch (codePoint.size()) {
            default:
                Q_UNREACHABLE(); // This can't happen, unless we got start or length wrong in std::string::substr()
                qWarning().nospace() << "TBuffer::processGBSequence(...) " << gbSequenceLength << "-byte " << (isGB18030 ? "GB18030" : "GB2312/GBK") << " sequence accepted, and it encoded to "
                                     << codePoint.size() << " QChars which does not make sense!!!";
                isValid = false;
                isToUseReplacementMark = true;
                break;
            case 2:
                isNonBmpCharacter = true;
            // Fall-through
                [[fallthrough]];
            case 1:
#if defined(DEBUG_GB_PROCESSING)
                qDebug().nospace() << "TBuffer::processGBSequence(...) " << gbSequenceLength << "-byte " << (isGB18030 ? "GB18030" : "GB2312/GBK") << " sequence accepted, it is " << codePoint.size()
                                   << " QChar(s) long [" << codePoint << "] and is in the " << dataIdentity.c_str() << " range";
#endif
                mMudLine.append(codePoint);
                break;
            case 0:
                qWarning().nospace() << "TBuffer::processGBSequence(...) " << gbSequenceLength << "-byte " << (isGB18030 ? "GB18030" : "GB2312/GBK")
                                     << " sequence accepted, but it did not encode to ANY QChar(s)!!!";
                isValid = false;
                isToUseReplacementMark = true;
            }
        } else {
            // Unable to decode it - no Qt decoder...!
            isValid = false;
            isToUseReplacementMark = true;
        }
    }

    if (!isValid) {
#if defined(DEBUG_GB_PROCESSING)
        QString debugMsg;
        for (size_t i = 0; i < gbSequenceLength; ++i) {
            debugMsg.append(qsl("<%1>").arg(static_cast<quint8>(bufferData.at(pos + i)), 2, 16, QChar('0')));
        }
        qDebug().nospace() << "    Sequence bytes are: " << debugMsg;
#endif
        if (isToUseReplacementMark) {
            mMudLine.append(QChar::ReplacementCharacter);
        }
    }

    // As there is already a unit increment at the bottom of loop
    // add one less than the sequence length:
    pos += gbSequenceLength - 1;

    return true;
}

bool TBuffer::processBig5Sequence(const std::string& bufferData, const bool isFromServer, const size_t len, size_t& pos, bool& isNonBmpCharacter)
{
#if defined(DEBUG_BIG5_PROCESSING)
    std::string dataIdentity;
#endif

    // The encoding standard are taken from https://en.wikipedia.org/wiki/Big5
    size_t big5SequenceLength = 1;
    bool isValid = true;
    bool isToUseReplacementMark = false;
    // Only set this if we are adding more than one code-point to
    // mCurrentLineCharacters:
    isNonBmpCharacter = false;
    if (static_cast<quint8>(bufferData.at(pos)) < 0x80) {
        // Is ASCII - single byte character, straight forward for a "first" byte case
        mMudLine.append(bufferData.at(pos));
        // As there is already a unit increment at the bottom of caller's loop
        // there is no need to tweak pos in THIS case

        return true;
    } else if (static_cast<quint8>(bufferData.at(pos)) == 0x80 || static_cast<quint8>(bufferData.at(pos)) > 0xFE) {
        // Invalid as first byte
        isValid = false;
        isToUseReplacementMark = true;
#if defined(DEBUG_BIG5_PROCESSING)
        qDebug().nospace() << "TBuffer::processBig5Sequence(...) 1-byte sequence as Big5 rejected!";
#endif
    } else {
        // We have two bytes
        big5SequenceLength = 2;
        if ((pos + big5SequenceLength - 1) >= len) {
            // Not enough bytes to process yet - so store what we have and return
            if (isFromServer) {
#if defined(DEBUG_BIG5_PROCESSING)
                qDebug().nospace() << "TBuffer::processBig5Sequence(...) Insufficient bytes in buffer to "
                                      "complete Big5 sequence, need at least: "
                                   << big5SequenceLength << " but we currently only have: " << bufferData.substr(pos).length() << " bytes (which we will store for next call to this method)...";
#endif
                mIncompleteSequenceBytes = bufferData.substr(pos);
            }
            return false; // Bail out
        } else {
            // check if second byte range is valid
            auto val2 = static_cast<quint8>(bufferData.at(pos + 1));
            if (val2 < 0x40 || (val2 > 0x7E && val2 < 0xA1) || val2 > 0xFE) {
                // second byte range is invalid
                isValid = false;
                isToUseReplacementMark = true;
            }
        }

    }

    // At this point we know how many bytes to consume, and whether they are in
    // the right ranges of individual values to be valid

    if (isValid) {
        // Try and convert two byte sequence to Unicode using Qts own
        // decoder - and check number of codepoints returned

        QString codePoint;
        if (TEncodingHelper::isEncodingAvailable(mEncoding)) {
            codePoint = TEncodingHelper::decode(QByteArray::fromRawData(bufferData.substr(pos, big5SequenceLength).c_str(), big5SequenceLength), mEncoding);
            switch (codePoint.size()) {
                default:
                    Q_UNREACHABLE(); // This can't happen, unless we got start or length wrong in std::string::substr()
                    qWarning().nospace() << "TBuffer::processBig5Sequence(...) " << big5SequenceLength << "-byte Big5 sequence accepted, and it encoded to "
                                         << codePoint.size() << " QChars which does not make sense!!!";
                    isValid = false;
                    isToUseReplacementMark = true;
                    break;
                case 2:
                    // Fall-through
                    [[fallthrough]];
                case 1:
                    // If Qt's decoder found bad characters, update status flags to reflect that.
                    if (codePoint.contains(QChar::ReplacementCharacter)) {
                        isValid = false;
                        isToUseReplacementMark = true;
                        break;
                    }
#if defined(DEBUG_BIG5_PROCESSING)
                    qDebug().nospace() << "TBuffer::processBig5Sequence(...) " << big5SequenceLength << "-byte Big5 sequence accepted, it is " << codePoint.size()
                                   << " QChar(s) long [" << codePoint << "] and is in the " << dataIdentity.c_str() << " range";
#endif
                    mMudLine.append(codePoint);
                    break;
                case 0:
                    qWarning().nospace() << "TBuffer::processBig5Sequence(...) " << big5SequenceLength << "-byte Big5"
                                   << "sequence accepted, but it did not encode to ANY QChar(s)!!!";
                    isValid = false;
                    isToUseReplacementMark = true;
            }
        } else {
            // Unable to decode it - no Qt decoder...!
#if defined(DEBUG_BIG5_PROCESSING)
            qDebug().nospace() << "No Qt decoder found...";
#endif
            isValid = false;
            isToUseReplacementMark = true;
        }
    }

    if (!isValid) {
#if defined(DEBUG_BIG5_PROCESSING)
        QString debugMsg;
        for (size_t i = 0; i < big5SequenceLength; ++i) {
            debugMsg.append(qsl("<%1>").arg(static_cast<quint8>(bufferData.at(pos + i)), 2, 16, QChar('0')));
        }
        qDebug().nospace() << "    Invalid.  Sequence bytes are: " << debugMsg;
#endif
        if (isToUseReplacementMark) {
            mMudLine.append(QChar::ReplacementCharacter);
        }
    }

    // As there is already a unit increment at the bottom of loop
    // add one less than the sequence length:
    pos += big5SequenceLength - 1;

    return true;
}

bool TBuffer::processEUC_KRSequence(const std::string& bufferData, const bool isFromServer, const size_t len, size_t& pos, bool& isNonBmpCharacter)
{
#if defined(DEBUG_EUC_KR_PROCESSING)
    std::string dataIdentity;
#endif

    // The encoding standard are taken from https://en.wikipedia.org/wiki/Extended_Unix_Code
    size_t eucSequenceLength = 1;
    bool isValid = true;
    bool isToUseReplacementMark = false;
    // Only set this if we are adding more than one code-point to
    // mCurrentLineCharacters:
    isNonBmpCharacter = false;
    if (static_cast<quint8>(bufferData.at(pos)) < 0x7F) {
        // Is ASCII - single byte character, straight forward for a "first" byte case
        mMudLine.append(bufferData.at(pos));
        // As there is already a unit increment at the bottom of caller's loop
        // there is no need to tweak pos in THIS case

        return true;
    } else if (static_cast<quint8>(bufferData.at(pos)) < 0xA1 || static_cast<quint8>(bufferData.at(pos)) == 0xFF) {
        // Invalid as first byte
        isValid = false;
        isToUseReplacementMark = true;
#if defined(DEBUG_EUC_KR_PROCESSING)
        qDebug().nospace() << "TBuffer::processEUC_KRSequence(...) 1-byte sequence as EUC-KR rejected!";
#endif
    } else {
        // We have two bytes
        eucSequenceLength = 2;
        if ((pos + eucSequenceLength - 1) >= len) {
            // Not enough bytes to process yet - so store what we have and return
            if (isFromServer) {
#if defined(DEBUG_EUC_KR_PROCESSING)
                    qDebug().nospace() << "TBuffer::processEUC_KRSequence(...) Insufficient bytes in buffer to "
                                          "complete EUC-KR sequence, need at least: "
                                       << eucSequenceLength << " but we currently only have: " << bufferData.substr(pos).length() << " bytes (which we will store for next call to this method)...";
#endif
                    mIncompleteSequenceBytes = bufferData.substr(pos);
            }
            return false; // Bail out
        } else {
            // check if second byte range is valid
            auto val2 = static_cast<quint8>(bufferData.at(pos + 1));
            if (val2 < 0xA1 || val2 == 0xFF) {
                    // second byte range is invalid
                    isValid = false;
                    isToUseReplacementMark = true;
            }
        }

    }

    // At this point we know how many bytes to consume, and whether they are in
    // the right ranges of individual values to be valid

    if (isValid) {
        // Try and convert two byte sequence to Unicode using Qts own
        // decoder - and check number of codepoints returned

        QString codePoint;
        if (TEncodingHelper::isEncodingAvailable(mEncoding)) {
            codePoint = TEncodingHelper::decode(QByteArray::fromRawData(bufferData.substr(pos, eucSequenceLength).c_str(), eucSequenceLength), mEncoding);
            switch (codePoint.size()) {
            default:
                    Q_UNREACHABLE(); // This can't happen, unless we got start or length wrong in std::string::substr()
                    qWarning().nospace() << "TBuffer::processEUC_KRSequence(...) " << eucSequenceLength << "-byte EUC-KR sequence accepted, and it encoded to "
                                         << codePoint.size() << " QChars which does not make sense!!!";
                    isValid = false;
                    isToUseReplacementMark = true;
                    break;
            case 2:
                    // Fall-through
                    [[fallthrough]];
            case 1:
                    // If Qt's decoder found bad characters, update status flags to reflect that.
                    if (codePoint.contains(QChar::ReplacementCharacter)) {
                        isValid = false;
                        isToUseReplacementMark = true;
                        break;
                    }
#if defined(DEBUG_EUC_KR_PROCESSING)
                    qDebug().nospace() << "TBuffer::processEUC_KRSequence(...) " << eucSequenceLength << "-byte EUC-KR sequence accepted, it is " << codePoint.size()
                                       << " QChar(s) long [" << codePoint << "] and is in the " << dataIdentity.c_str() << " range";
#endif
                    mMudLine.append(codePoint);
                    break;
            case 0:
                    qWarning().nospace() << "TBuffer::processEUC_KRSequence(...) " << eucSequenceLength << "-byte EUC-KR"
                                         << "sequence accepted, but it did not encode to ANY QChar(s)!!!";
                    isValid = false;
                    isToUseReplacementMark = true;
            }
        } else {
            // Unable to decode it - no Qt decoder...!
#if defined(DEBUG_EUC_KR_PROCESSING)
            qDebug().nospace() << "No Qt decoder found...";
#endif
            isValid = false;
            isToUseReplacementMark = true;
        }
    }

    if (!isValid) {
#if defined(DEBUG_EUC_KR_PROCESSING)
        QString debugMsg;
        for (size_t i = 0; i < eucSequenceLength; ++i) {
            debugMsg.append(qsl("<%1>").arg(static_cast<quint8>(bufferData.at(pos + i)), 2, 16, QChar('0')));
        }
        qDebug().nospace() << "    Invalid.  Sequence bytes are: " << debugMsg;
#endif
        if (isToUseReplacementMark) {
            mMudLine.append(QChar::ReplacementCharacter);
        }
    }

    // As there is already a unit increment at the bottom of loop
    // add one less than the sequence length:
    pos += eucSequenceLength - 1;

    return true;
}

void TBuffer::encodingChanged(const QByteArray& newEncoding)
{
    if (mEncoding != newEncoding) {
        mEncoding = newEncoding;
        if (mEncoding == "GBK" || mEncoding == "GB18030" || mEncoding == "BIG5" || mEncoding == "BIG5-HKSCS" || mEncoding == "EUC-KR") {
            if (!TEncodingHelper::isEncodingAvailable(mEncoding)) {
                qCritical().nospace() << "encodingChanged(" << newEncoding << ") ERROR: This encoding cannot be handled as a required codec was not found in the system!";
            } else {
                qDebug().nospace() << "encodingChanged(" << newEncoding << ") INFO: Encoding is available and will be used.";
            }
        }
    }
}

// Count the graphemes in a QString - returning its length in terms of those:
int TBuffer::lengthInGraphemes(const QString& text)
{
    if (text.isEmpty()) {
        return 0;
    }

    QTextBoundaryFinder graphemeFinder(QTextBoundaryFinder::Grapheme, text);
    int pos = graphemeFinder.toNextBoundary();
    int count = 0;
    while (pos > 0) {
        ++count;
        pos = graphemeFinder.toNextBoundary();
    }
    return count;
}

const QList<QByteArray> TBuffer::getEncodingNames()
{
     return csmEncodingTable.getEncodingNames();
}

void TBuffer::clearSearchHighlights()
{
    for (auto& line : buffer) {
        for (auto& character : line) {
            character.mFlags &= ~TChar::AttributeFlag::Found;
        }
    }
}

void TBuffer::injectOSC8DocumentationExamples()
{
    // Inject OSC 8 hyperlink examples matching the documentation structure
#if defined(DEBUG_OSC_PROCESSING)
    qDebug() << "[OSC8] injectOSC8DocumentationExamples() called";
#endif

    QString output = "\n╔════════════════════════════════════════════════════════════════════╗\n";
    output += "║         OSC 8 Hyperlink Documentation Examples                     ║\n";
    output += "╚════════════════════════════════════════════════════════════════════╝\n\n";

    // ═══════════════════════════════════════════════════════════════════
    // Getting Started: Simple Links
    // ═══════════════════════════════════════════════════════════════════
    output += "══════════════════════════════════════════════════════════════════════\n";
    output += "GETTING STARTED: SIMPLE LINKS\n";
    output += "══════════════════════════════════════════════════════════════════════\n\n";

    output += "1. Sending Commands:\n";
    output += "   \x1b]8;;send:look\x1b\\Look\x1b]8;;\x1b\\\n\n";

    output += "2. Pre-filling Commands:\n";
    output += "   \x1b]8;;prompt:cast%20fireball\x1b\\Cast Fireball\x1b]8;;\x1b\\\n\n";

    output += "3. Opening Web Pages:\n";
    output += "   \x1b]8;;https://www.mudlet.org\x1b\\Visit Mudlet\x1b]8;;\x1b\\\n\n";

    // ═══════════════════════════════════════════════════════════════════
    // Adding Custom Styling
    // ═══════════════════════════════════════════════════════════════════
    output += "══════════════════════════════════════════════════════════════════════\n";
    output += "ADDING CUSTOM STYLING\n";
    output += "══════════════════════════════════════════════════════════════════════\n\n";

    output += "Basic Styling:\n";
    output += "• Red text: \x1b]8;;send:attack?config={\"style\":{\"color\":\"red\"}}\x1b\\Attack\x1b]8;;\x1b\\\n";
    output += "• Bold red: \x1b]8;;send:attack?config={\"style\":{\"color\":\"red\",\"bold\":true}}\x1b\\Attack\x1b]8;;\x1b\\\n";
    output += "• Blue background: \x1b]8;;send:action?config={\"style\":{\"color\":\"white\",\"bg\":\"blue\"}}\x1b\\Action\x1b]8;;\x1b\\\n\n";

    output += "Text Decorations:\n";
    output += "• Underlined: \x1b]8;;send:cast?config={\"style\":{\"underline\":true}}\x1b\\Underlined\x1b]8;;\x1b\\\n";
    output += "• Overlined: \x1b]8;;send:heal?config={\"style\":{\"overline\":true}}\x1b\\Overlined\x1b]8;;\x1b\\\n";
    output += "• Strikethrough: \x1b]8;;send:flee?config={\"style\":{\"strikethrough\":true}}\x1b\\Strikethrough\x1b]8;;\x1b\\\n";
    output += "• Wavy underline: \x1b]8;;send:spell?config={\"style\":{\"underline\":\"wavy\"}}\x1b\\Wavy Underline\x1b]8;;\x1b\\\n";
    output += "• Dotted overline: \x1b]8;;send:magic?config={\"style\":{\"overline\":\"dotted\"}}\x1b\\Dotted Overline\x1b]8;;\x1b\\\n\n";

    output += "Decoration Colors:\n";
    output += "• Red underline: \x1b]8;;send:redline?config={\"style\":{\"underline\":true,\"text-decoration-color\":\"red\"}}\x1b\\Red Underline\x1b]8;;\x1b\\\n";
    output += "• Blue overline: \x1b]8;;send:blueover?config={\"style\":{\"overline\":true,\"text-decoration-color\":\"#0066ff\"}}\x1b\\Blue Overline\x1b]8;;\x1b\\\n";
    output += "• Green wavy: \x1b]8;;send:greenwave?config={\"style\":{\"underline\":\"wavy\",\"text-decoration-color\":\"green\"}}\x1b\\Green Wavy\x1b]8;;\x1b\\\n";
    output += "• Purple strike: \x1b]8;;send:purplestrike?config={\"style\":{\"strikethrough\":true,\"text-decoration-color\":\"purple\"}}\x1b\\Purple Strike\x1b]8;;\x1b\\\n\n";

    // ═══════════════════════════════════════════════════════════════════
    // Interactive States
    // ═══════════════════════════════════════════════════════════════════
    output += "══════════════════════════════════════════════════════════════════════\n";
    output += "INTERACTIVE STATES\n";
    output += "══════════════════════════════════════════════════════════════════════\n\n";

    output += "Hover Effects:\n";
    output += "• Hover color change: \x1b]8;;send:look?config={\"style\":{\"color\":\"blue\",\"hover\":{\"color\":\"red\"}}}\x1b\\Hover Me\x1b]8;;\x1b\\\n";
    output += "• Hover bold: \x1b]8;;send:defend?config={\"style\":{\"color\":\"blue\",\"bold\":true,\"hover\":{\"color\":\"yellow\"}}}\x1b\\Defend\x1b]8;;\x1b\\\n\n";

    output += "Link vs Visited States:\n";
    output += "• \x1b]8;;send:explore?config={\"style\":{\"link\":{\"color\":\"blue\",\"underline\":true},\"visited\":{\"color\":\"purple\",\"strikethrough\":true}}}\x1b\\Visit Me\x1b]8;;\x1b\\\n\n";

    output += "Button-Style Link:\n";
    output += "• \x1b]8;;send:attack?config={\"style\":{\"bg\":\"green\",\"color\":\"white\",\"bold\":true,\"hover\":{\"bg\":\"lightgreen\"},\"active\":{\"bg\":\"darkgreen\"}}}\x1b\\Attack\x1b]8;;\x1b\\\n\n";

    output += "Multi-State Button:\n";
    output += "• \x1b]8;;send:action?config={\"style\":{\"bg\":\"green\",\"hover\":{\"bg\":\"lightgreen\"},\"active\":{\"bg\":\"darkgreen\"},\"focus-visible\":{\"bg\":\"lightblue\"}}}\x1b\\Multi-State\x1b]8;;\x1b\\\n\n";

    output += "Any-Link Styling:\n";
    output += "• \x1b]8;;send:universal?config={\"style\":{\"any-link\":{\"bold\":true},\"hover\":{\"color\":\"orange\"}}}\x1b\\Universal Link\x1b]8;;\x1b\\\n\n";

    // ═══════════════════════════════════════════════════════════════════
    // Context Menus and Tooltips
    // ═══════════════════════════════════════════════════════════════════
    output += "══════════════════════════════════════════════════════════════════════\n";
    output += "CONTEXT MENUS AND TOOLTIPS\n";
    output += "══════════════════════════════════════════════════════════════════════\n\n";

    output += "Basic Menu:\n";
    output += "• \x1b]8;;send:combat?config={\"menu\":[{\"Attack\":\"send:attack\"},{\"Defend\":\"send:defend\"}]}\x1b\\Combat Menu\x1b]8;;\x1b\\ (right-click for menu)\n\n";

    output += "Menu with Tooltip:\n";
    output += "• \x1b]8;;send:magic?config={\"menu\":[{\"Cast\":\"send:cast fireball\"},{\"Heal\":\"send:heal\"}],\"tooltip\":\"Magic actions\"}\x1b\\Magic Menu\x1b]8;;\x1b\\\n\n";

    output += "Menu with Separators:\n";
    output += "• \x1b]8;;send:spells?config={\"menu\":[{\"Fireball\":\"send:cast fireball\"},{\"Ice Bolt\":\"send:cast ice bolt\"},\"-\",{\"Heal\":\"send:cast heal\"}]}\x1b\\Spell Menu\x1b]8;;\x1b\\\n\n";

    output += "Primary vs Secondary Actions:\n";
    output += "• \x1b]8;;send:look?config={\"menu\":[{\"Look\":\"send:look\"},{\"Examine\":\"send:examine\"},\"-\",{\"Go North\":\"send:north\"}]}\x1b\\Look Around\x1b]8;;\x1b\\\n";
    output += "  (Left-click executes 'look', right-click shows menu)\n\n";

    output += "Combined Styling and Menus:\n";
    output += "• \x1b]8;;send:attack?config={\"style\":{\"color\":\"red\",\"bold\":true},\"menu\":[{\"Attack\":\"send:attack\"},{\"Defend\":\"send:defend\"},{\"Heal\":\"send:heal\"}],\"tooltip\":\"Combat Actions\"}\x1b\\Combat Menu\x1b]8;;\x1b\\\n\n";

    // ═══════════════════════════════════════════════════════════════════
    // Practical Examples
    // ═══════════════════════════════════════════════════════════════════
    output += "══════════════════════════════════════════════════════════════════════\n";
    output += "PRACTICAL EXAMPLES\n";
    output += "══════════════════════════════════════════════════════════════════════\n\n";

    output += "Simple Styled Links:\n";
    output += "• Red attack link: \x1b]8;;send:attack?config={\"style\":{\"color\":\"red\"}}\x1b\\Attack\x1b]8;;\x1b\\\n";
    output += "• Bold blue with hover: \x1b]8;;send:defend?config={\"style\":{\"color\":\"blue\",\"bold\":true,\"hover\":{\"color\":\"yellow\"}}}\x1b\\Defend\x1b]8;;\x1b\\\n";
    output += "• Green wavy underline: \x1b]8;;send:spell?config={\"style\":{\"underline\":\"wavy\",\"text-decoration-color\":\"green\"}}\x1b\\Spell Link\x1b]8;;\x1b\\\n\n";

    output += "Interactive Menu Examples:\n";
    output += "• Basic combat menu: \x1b]8;;send:combat?config={\"menu\":[{\"Attack\":\"send:attack\"},{\"Defend\":\"send:defend\"}]}\x1b\\Combat\x1b]8;;\x1b\\\n";
    output += "• Menu with separators: \x1b]8;;send:spells?config={\"menu\":[{\"Fireball\":\"send:cast fireball\"},{\"Ice Bolt\":\"send:cast ice bolt\"},\"-\",{\"Heal\":\"send:cast heal\"}]}\x1b\\Spells\x1b]8;;\x1b\\\n";
    output += "• Menu with tooltip: \x1b]8;;send:magic?config={\"menu\":[{\"Cast\":\"send:cast fireball\"},{\"Heal\":\"send:heal\"}],\"tooltip\":\"Magic actions\"}\x1b\\Magic\x1b]8;;\x1b\\\n\n";

    output += "Button-Style Links:\n";
    output += "• \x1b]8;;send:attack?config={\"style\":{\"bg\":\"green\",\"color\":\"white\",\"bold\":true,\"hover\":{\"bg\":\"lightgreen\"},\"active\":{\"bg\":\"darkgreen\"}}}\x1b\\Attack Button\x1b]8;;\x1b\\\n\n";

    // ═══════════════════════════════════════════════════════════════════
    // Advanced Features
    // ═══════════════════════════════════════════════════════════════════
    output += "══════════════════════════════════════════════════════════════════════\n";
    output += "ADVANCED FEATURES\n";
    output += "══════════════════════════════════════════════════════════════════════\n\n";

    output += "ANSI + JSON Hybrid Styling:\n";
    output += "• \x1b[1m\x1b]8;;send:attack?config={\"style\":{\"color\":\"red\",\"hover\":{\"color\":\"yellow\",\"underline\":true}}}\x1b\\Bold Red Attack\x1b]8;;\x1b\\\x1b[0m\n";
    output += "• \x1b[38;5;196m\x1b]8;;send:fire?config={\"style\":{\"hover\":{\"bg\":\"black\"},\"active\":{\"color\":\"orange\"}}}\x1b\\Fire Magic\x1b]8;;\x1b\\\x1b[0m\n\n";

    output += "Comprehensive Multi-State Example:\n";
    output += "• \x1b]8;;send:comprehensive?config={\"style\":{\"any-link\":{\"underline\":true},\"link\":{\"color\":\"blue\"},\"visited\":{\"color\":\"purple\"},\"hover\":{\"color\":\"red\"},\"active\":{\"color\":\"darkred\"},\"focus-visible\":{\"bg\":\"orange\",\"color\":\"white\"}}}\x1b\\Comprehensive Link\x1b]8;;\x1b\\\n\n";

    output += "Color Format Examples:\n";
    output += "• Hex: \x1b]8;;send:hex?config={\"style\":{\"color\":\"#ff0000\"}}\x1b\\Red (#ff0000)\x1b]8;;\x1b\\\n";
    output += "• Shorthand: \x1b]8;;send:short?config={\"style\":{\"color\":\"#f00\"}}\x1b\\Red (#f00)\x1b]8;;\x1b\\\n";
    output += "• Named: \x1b]8;;send:named?config={\"style\":{\"color\":\"red\"}}\x1b\\Red (named)\x1b]8;;\x1b\\\n";
    output += "• RGB: \x1b]8;;send:rgb?config={\"style\":{\"color\":\"rgb(255,0,0)\"}}\x1b\\Red (RGB)\x1b]8;;\x1b\\\n\n";

    // ═══════════════════════════════════════════════════════════════════
    // Real World Examples
    // ═══════════════════════════════════════════════════════════════════
    output += "══════════════════════════════════════════════════════════════════════\n";
    output += "REAL WORLD EXAMPLES\n";
    output += "══════════════════════════════════════════════════════════════════════\n\n";

    output += "Navigation:\n";
    output += "  \x1b]8;;send:north?config={\"style\":{\"color\":\"#0066cc\",\"bold\":true,\"hover\":{\"color\":\"#3399ff\",\"underline\":true}}}\x1b\\North\x1b]8;;\x1b\\ ";
    output += "\x1b]8;;send:south?config={\"style\":{\"color\":\"#0066cc\",\"bold\":true,\"hover\":{\"color\":\"#3399ff\",\"underline\":true}}}\x1b\\South\x1b]8;;\x1b\\ ";
    output += "\x1b]8;;send:east?config={\"style\":{\"color\":\"#0066cc\",\"bold\":true,\"hover\":{\"color\":\"#3399ff\",\"underline\":true}}}\x1b\\East\x1b]8;;\x1b\\ ";
    output += "\x1b]8;;send:west?config={\"style\":{\"color\":\"#0066cc\",\"bold\":true,\"hover\":{\"color\":\"#3399ff\",\"underline\":true}}}\x1b\\West\x1b]8;;\x1b\\\n\n";

    output += "Combat:\n";
    output += "  \x1b]8;;send:attack?config={\"style\":{\"color\":\"#cc0000\",\"bold\":true,\"hover\":{\"bg\":\"#ffcccc\",\"color\":\"#000000\"}},\"menu\":[{\"Quick Strike\":\"send:quick\"},{\"Power Attack\":\"send:power\"},\"-\",{\"Feint\":\"send:feint\"}]}\x1b\\⚔️ Attack\x1b]8;;\x1b\\ ";
    output += "\x1b]8;;send:defend?config={\"style\":{\"color\":\"#006600\",\"bold\":true,\"hover\":{\"bg\":\"#ccffcc\",\"color\":\"#000000\"}},\"tooltip\":\"Defensive stance - reduces damage by 50%\"}\x1b\\🛡️ Defend\x1b]8;;\x1b\\\n\n";

    output += "RPG Items:\n";
    output += "  \x1b]8;;send:examine?config={\"style\":{\"color\":\"#cc6600\",\"italic\":true,\"hover\":{\"bg\":\"#ffe6cc\",\"color\":\"#000000\"}},\"tooltip\":\"Magic sword with fire enchantment\"}\x1b\\🗡️ Flaming Blade\x1b]8;;\x1b\\ ";
    output += "\x1b]8;;send:bag?config={\"style\":{\"color\":\"#6600cc\",\"hover\":{\"color\":\"#9933ff\",\"underline\":true}},\"menu\":[{\"Open\":\"send:open\"},{\"Sort\":\"send:sort\"},\"-\",{\"Drop\":\"send:drop\"}]}\x1b\\🎒 Backpack\x1b]8;;\x1b\\\n\n";

    output += "Status Indicators:\n";
    output += "  \x1b]8;;send:warning?config={\"style\":{\"bg\":\"#ffcc00\",\"color\":\"#000000\",\"bold\":true}}\x1b\\⚠️ Warning\x1b]8;;\x1b\\ ";
    output += "\x1b]8;;send:error?config={\"style\":{\"bg\":\"#cc0000\",\"color\":\"#ffffff\",\"bold\":true}}\x1b\\❌ Error\x1b]8;;\x1b\\ ";
    output += "\x1b]8;;send:success?config={\"style\":{\"bg\":\"#006600\",\"color\":\"#ffffff\",\"bold\":true}}\x1b\\✅ Success\x1b]8;;\x1b\\\n\n";

    output += "Complete Example (All Features):\n";
    output += "  \x1b]8;;send:sword?config={\"style\":{\"color\":\"#cc6600\",\"bold\":true,\"hover\":{\"bg\":\"#ffcc99\",\"color\":\"#000000\"}},\"menu\":[{\"Equip\":\"send:equip\"},{\"Examine\":\"send:examine\"},\"-\",{\"Drop\":\"send:drop\"}],\"tooltip\":\"Flaming Sword: +5 damage, fire enchantment\"}\x1b\\🗡️ Flaming Sword\x1b]8;;\x1b\\\n\n";

    // ═══════════════════════════════════════════════════════════════════
    // Summary
    // ═══════════════════════════════════════════════════════════════════
    output += "══════════════════════════════════════════════════════════════════════\n\n";
    output += "Documentation: https://wiki.mudlet.org/w/Manual:Supported_Protocols#OSC_8\n";
    output += "All examples above are clickable - try them!\n\n";

    // Process the output through the normal text processing pipeline
    std::string outputBytes = output.toStdString();
#if defined(DEBUG_OSC_PROCESSING)
    qDebug() << "[OSC8] About to process documentation examples through translateToPlainText, length:" << outputBytes.length();
#endif
    translateToPlainText(outputBytes, true); // Mark as from server
}

// ============================================================================
// CSS Link States Implementation with Accessibility Best Practices
// ============================================================================

Mudlet::HyperlinkStyling::StateStyle Mudlet::HyperlinkStyling::getEffectiveStyle() const
{
    Mudlet::HyperlinkStyling::StateStyle effective;

    // Start with base/default styling
    effective.foregroundColor = foregroundColor;
    effective.backgroundColor = backgroundColor;
    effective.underlineColor = underlineColor;
    effective.overlineColor = overlineColor;
    effective.strikeoutColor = strikeoutColor;
    effective.hasForegroundColor = hasForegroundColor;
    effective.hasBackgroundColor = hasBackgroundColor;
    effective.hasUnderlineColor = hasUnderlineColor;
    effective.hasOverlineColor = hasOverlineColor;
    effective.hasStrikeoutColor = hasStrikeoutColor;
    effective.isBold = isBold;
    effective.isItalic = isItalic;
    effective.isUnderlined = isUnderlined;
    effective.isStrikeOut = isStrikeOut;
    effective.isOverlined = isOverlined;
    effective.underlineStyle = underlineStyle;

    // Apply :any-link styles (applies to both :link and :visited)
    if (anyLinkStyle.hasCustomStyling) {
        if (anyLinkStyle.hasForegroundColor) effective.foregroundColor = anyLinkStyle.foregroundColor;
        if (anyLinkStyle.hasBackgroundColor) effective.backgroundColor = anyLinkStyle.backgroundColor;
        if (anyLinkStyle.hasUnderlineColor) effective.underlineColor = anyLinkStyle.underlineColor;
        if (anyLinkStyle.hasOverlineColor) effective.overlineColor = anyLinkStyle.overlineColor;
        if (anyLinkStyle.hasStrikeoutColor) effective.strikeoutColor = anyLinkStyle.strikeoutColor;
        effective.isBold = anyLinkStyle.isBold;
        effective.isItalic = anyLinkStyle.isItalic;
        effective.isUnderlined = anyLinkStyle.isUnderlined;
        effective.isStrikeOut = anyLinkStyle.isStrikeOut;
        effective.isOverlined = anyLinkStyle.isOverlined;
        effective.underlineStyle = anyLinkStyle.underlineStyle;
    }

    // Apply state-specific styles with proper cascade order
    const StateStyle* stateStyle = nullptr;

    switch (currentState) {
        case StateVisited:
            if (visitedStyle.hasCustomStyling) stateStyle = &visitedStyle;
            break;
        case StateHover:
            if (hoverStyle.hasCustomStyling) stateStyle = &hoverStyle;
            break;
        case StateActive:
            if (activeStyle.hasCustomStyling) stateStyle = &activeStyle;
            break;
        case StateFocus:
            if (focusStyle.hasCustomStyling) stateStyle = &focusStyle;
            break;
        case StateFocusVisible:
            if (focusVisibleStyle.hasCustomStyling) stateStyle = &focusVisibleStyle;
            break;
        case StateDefault:
        default:
            if (linkStyle.hasCustomStyling) stateStyle = &linkStyle;
            break;
    }

    // Apply the selected state style
    if (stateStyle) {
        if (stateStyle->hasForegroundColor) {
            effective.foregroundColor = stateStyle->foregroundColor;
            effective.hasForegroundColor = true;
        }
        if (stateStyle->hasBackgroundColor) {
            effective.backgroundColor = stateStyle->backgroundColor;
            effective.hasBackgroundColor = true;
        }
        if (stateStyle->hasUnderlineColor) {
            effective.underlineColor = stateStyle->underlineColor;
            effective.hasUnderlineColor = true;
        }
        if (stateStyle->hasOverlineColor) {
            effective.overlineColor = stateStyle->overlineColor;
            effective.hasOverlineColor = true;
        }
        if (stateStyle->hasStrikeoutColor) {
            effective.strikeoutColor = stateStyle->strikeoutColor;
            effective.hasStrikeoutColor = true;
        }
        effective.isBold = stateStyle->isBold;
        effective.isItalic = stateStyle->isItalic;
        effective.isUnderlined = stateStyle->isUnderlined;
        effective.isStrikeOut = stateStyle->isStrikeOut;
        effective.isOverlined = stateStyle->isOverlined;
        effective.underlineStyle = stateStyle->underlineStyle;
        effective.hasCustomStyling = true;
    }

    // Even if the current state doesn't have custom styling, we need to mark that
    // the link has SOME pseudo-class styling so updateLinkCharacters() processes it.
    // This ensures ANSI base links with only :hover (but no :link) styling get updated.
    if (!effective.hasCustomStyling) {
        // Check if ANY pseudo-class state has custom styling
        if (linkStyle.hasCustomStyling || visitedStyle.hasCustomStyling ||
            hoverStyle.hasCustomStyling || activeStyle.hasCustomStyling ||
            focusStyle.hasCustomStyling || focusVisibleStyle.hasCustomStyling ||
            anyLinkStyle.hasCustomStyling) {
            effective.hasCustomStyling = true;
        }
    }

    return effective;
}

void TBuffer::parseHyperlinkStateStyle(const QString& pseudoClass, const QString& styleString, Mudlet::HyperlinkStyling& styling)
{
#if defined(DEBUG_OSC_PROCESSING)
    qDebug() << "[OSC8] parseHyperlinkStateStyle called with pseudoClass:" << pseudoClass
             << "styleString:" << styleString;
#endif

    QString cleanPseudoClass = pseudoClass.trimmed().toLower();
    Mudlet::HyperlinkStyling::StateStyle* targetStyle = nullptr;

    // Map pseudo-class selectors to appropriate state styles
    if (cleanPseudoClass == qsl(":link")) {
        targetStyle = &styling.linkStyle;
    } else if (cleanPseudoClass == qsl(":visited")) {
        targetStyle = &styling.visitedStyle;
    } else if (cleanPseudoClass == qsl(":hover")) {
        targetStyle = &styling.hoverStyle;
    } else if (cleanPseudoClass == qsl(":active")) {
        targetStyle = &styling.activeStyle;
    } else if (cleanPseudoClass == qsl(":focus")) {
        targetStyle = &styling.focusStyle;
    } else if (cleanPseudoClass == qsl(":focus-visible")) {
        targetStyle = &styling.focusVisibleStyle;
    } else if (cleanPseudoClass == qsl(":any-link")) {
        targetStyle = &styling.anyLinkStyle;
    } else {
#if defined(DEBUG_OSC_PROCESSING)
        qDebug() << "[OSC8] Unknown pseudo-class:" << cleanPseudoClass;
#endif
        return;
    }

    if (targetStyle) {
        parseStateStyleProperties(styleString, *targetStyle);
        applyAccessibilityEnhancements(styling);
    }
}

void TBuffer::parseStateStyleProperties(const QString& styleString, Mudlet::HyperlinkStyling::StateStyle& stateStyle)
{
    // Parse CSS-like style string: "property:value;property:value"
    QStringList stylePairs = styleString.split(';', Qt::SkipEmptyParts);

    bool hasAnyCustomStyling = false;

    for (const QString& pair : stylePairs) {
        QStringList propertyValue = pair.split(':', Qt::KeepEmptyParts);
        if (propertyValue.size() != 2) {
            continue; // Skip malformed property
        }

        QString property = propertyValue[0].trimmed().toLower();
        QString value = propertyValue[1].trimmed();

        if (property == qsl("color")) {
            QColor color = parseColorValue(value);
            if (color.isValid()) {
                stateStyle.foregroundColor = color;
                stateStyle.hasForegroundColor = true;
                hasAnyCustomStyling = true;
            }
        } else if (property == qsl("background") || property == qsl("background-color")) {
            // Support both "background" and "background-color" properties
            QColor color = parseColorValue(value);
            if (color.isValid()) {
                stateStyle.backgroundColor = color;
                stateStyle.hasBackgroundColor = true;
                hasAnyCustomStyling = true;
            }
        } else if (property == qsl("font-weight")) {
            stateStyle.isBold = (value.toLower() == qsl("bold") || value == "700" || value.toInt() >= 700);
            hasAnyCustomStyling = true;
        } else if (property == qsl("font-style")) {
            stateStyle.isItalic = (value.toLower() == qsl("italic"));
            hasAnyCustomStyling = true;
        } else if (property == qsl("text-decoration")) {
            // Handle text-decoration with optional style
            QStringList decorationParts = value.toLower().split(' ', Qt::SkipEmptyParts);

            // Reset decoration flags
            stateStyle.isUnderlined = false;
            stateStyle.isOverlined = false;
            stateStyle.isStrikeOut = false;
            stateStyle.underlineStyle = Mudlet::HyperlinkStyling::UnderlineNone;

            for (const QString& part : decorationParts) {
                if (part == qsl("underline")) {
                    stateStyle.isUnderlined = true;
                    stateStyle.underlineStyle = Mudlet::HyperlinkStyling::UnderlineSolid;
                } else if (part == qsl("overline")) {
                    stateStyle.isOverlined = true;
                } else if (part == qsl("line-through")) {
                    stateStyle.isStrikeOut = true;
                } else if (part == qsl("none")) {
                    stateStyle.isUnderlined = false;
                    stateStyle.isOverlined = false;
                    stateStyle.isStrikeOut = false;
                } else if (part == qsl("wavy") && stateStyle.isUnderlined) {
                    stateStyle.underlineStyle = Mudlet::HyperlinkStyling::UnderlineWavy;
                } else if (part == qsl("dotted") && stateStyle.isUnderlined) {
                    stateStyle.underlineStyle = Mudlet::HyperlinkStyling::UnderlineDotted;
                } else if (part == qsl("dashed") && stateStyle.isUnderlined) {
                    stateStyle.underlineStyle = Mudlet::HyperlinkStyling::UnderlineDashed;
                } else {
                    // Check if it's a color for the decoration
                    QColor decorationColor = parseColorValue(part);
                    if (decorationColor.isValid()) {
                        if (stateStyle.isUnderlined) {
                            stateStyle.underlineColor = decorationColor;
                            stateStyle.hasUnderlineColor = true;
                        } else if (stateStyle.isOverlined) {
                            stateStyle.overlineColor = decorationColor;
                            stateStyle.hasOverlineColor = true;
                        } else if (stateStyle.isStrikeOut) {
                            stateStyle.strikeoutColor = decorationColor;
                            stateStyle.hasStrikeoutColor = true;
                        }
                    }
                }
            }
            hasAnyCustomStyling = true;
        } else if (property == qsl("text-decoration-color")) {
            QColor decorationColor = parseColorValue(value);
            if (decorationColor.isValid()) {
                stateStyle.underlineColor = decorationColor;
                stateStyle.hasUnderlineColor = true;
                stateStyle.overlineColor = decorationColor;
                stateStyle.hasOverlineColor = true;
                stateStyle.strikeoutColor = decorationColor;
                stateStyle.hasStrikeoutColor = true;
                hasAnyCustomStyling = true;
            }
        }
    }

    stateStyle.hasCustomStyling = hasAnyCustomStyling;
}

void TBuffer::applyAccessibilityEnhancements(Mudlet::HyperlinkStyling& styling)
{
    // Ensure focus states are appropriately visible
    if (!styling.focusStyle.hasCustomStyling && !styling.focusVisibleStyle.hasCustomStyling) {
        // Provide default focus styling for accessibility
        styling.focusStyle.hasCustomStyling = true;
        styling.focusStyle.isUnderlined = true;
        styling.focusStyle.underlineStyle = Mudlet::HyperlinkStyling::UnderlineSolid;
        styling.focusStyle.hasUnderlineColor = true;
        styling.focusStyle.underlineColor = QColor(0, 120, 215); // Windows accent blue

        styling.focusVisibleStyle = styling.focusStyle; // Copy focus style to focus-visible
    }
}

// Link state management methods for interactive pseudo-classes
void TBuffer::setLinkState(int linkIndex, Mudlet::HyperlinkStyling::LinkState state)
{
    if (linkIndex <= 0) {
        return; // Invalid link index
    }

    mLinkStates[linkIndex] = state;

#if defined(DEBUG_OSC_PROCESSING)
    QString stateName;
    switch (state) {
        case Mudlet::HyperlinkStyling::StateDefault: stateName = "Default"; break;
        case Mudlet::HyperlinkStyling::StateVisited: stateName = "Visited"; break;
        case Mudlet::HyperlinkStyling::StateHover: stateName = "Hover"; break;
        case Mudlet::HyperlinkStyling::StateActive: stateName = "Active"; break;
        case Mudlet::HyperlinkStyling::StateFocus: stateName = "Focus"; break;
        case Mudlet::HyperlinkStyling::StateFocusVisible: stateName = "FocusVisible"; break;
    }
    qDebug() << "[OSC8] Link" << linkIndex << "state changed to:" << stateName;
#endif
}

Mudlet::HyperlinkStyling::LinkState TBuffer::getLinkState(int linkIndex) const
{
    if (linkIndex <= 0) {
        return Mudlet::HyperlinkStyling::StateDefault;
    }
    return mLinkStates.value(linkIndex, Mudlet::HyperlinkStyling::StateDefault);
}

Mudlet::HyperlinkStyling TBuffer::getEffectiveHyperlinkStyling(int linkIndex) const
{
    if (linkIndex <= 0) {
        return Mudlet::HyperlinkStyling(); // Return default styling
    } else {
        // Get the stored styling for this link from mLinkStore
        Mudlet::HyperlinkStyling styling = mLinkStore.getStyling(linkIndex);

        // Update the current state based on tracked state
        styling.currentState = getLinkState(linkIndex);

        // The HyperlinkStyling::getEffectiveStyle() method will compute the
        // effective styling based on currentState, cascading from base -> :any-link -> state-specific
        // However, for rendering we need to apply it to the base styling properties
        // so the rendering code can use it directly

        auto effective = styling.getEffectiveStyle();

        // Copy effective state back to base properties for rendering
        styling.foregroundColor = effective.foregroundColor;
        styling.backgroundColor = effective.backgroundColor;
        styling.underlineColor = effective.underlineColor;
        styling.overlineColor = effective.overlineColor;
        styling.strikeoutColor = effective.strikeoutColor;
        styling.hasForegroundColor = effective.hasForegroundColor;
        styling.hasBackgroundColor = effective.hasBackgroundColor;
        styling.hasUnderlineColor = effective.hasUnderlineColor;
        styling.hasOverlineColor = effective.hasOverlineColor;
        styling.hasStrikeoutColor = effective.hasStrikeoutColor;
        styling.isBold = effective.isBold;
        styling.isItalic = effective.isItalic;
        styling.isUnderlined = effective.isUnderlined;
        styling.isStrikeOut = effective.isStrikeOut;
        styling.isOverlined = effective.isOverlined;
        styling.underlineStyle = effective.underlineStyle;
        styling.hasCustomStyling = effective.hasCustomStyling; // CRITICAL: Copy hasCustomStyling flag

        return styling;
    }
}

void TBuffer::setHoveredLink(int linkIndex)
{
    int previousHoveredLink = mCurrentHoveredLinkIndex;
    mCurrentHoveredLinkIndex = linkIndex;

    // Reset previous hovered link to its base state (default or visited)
    if (previousHoveredLink > 0 && previousHoveredLink != linkIndex) {
        auto currentState = getLinkState(previousHoveredLink);

        if (currentState == Mudlet::HyperlinkStyling::StateHover) {
            // Return to visited state if it was visited, otherwise to default
            if (isLinkVisited(previousHoveredLink)) {
                setLinkState(previousHoveredLink, Mudlet::HyperlinkStyling::StateVisited);
            } else {
                setLinkState(previousHoveredLink, Mudlet::HyperlinkStyling::StateDefault);
            }

            updateLinkCharacters(previousHoveredLink); // Update displayed characters
        }
    }

    // Set new link to hover state
    if (linkIndex > 0) {
        auto currentState = getLinkState(linkIndex);
        // Don't override active state with hover
        // But we can hover over visited links to show hover styling
        if (currentState != Mudlet::HyperlinkStyling::StateActive) {
            setLinkState(linkIndex, Mudlet::HyperlinkStyling::StateHover);
            updateLinkCharacters(linkIndex); // Update displayed characters
        }
    }
}

void TBuffer::setActiveLink(int linkIndex)
{
    int previousActiveLink = mCurrentActiveLinkIndex;
    mCurrentActiveLinkIndex = linkIndex;

    // Reset previous active link
    if (previousActiveLink > 0 && previousActiveLink != linkIndex) {

        // Return to hover if it's still hovered
        if (previousActiveLink == mCurrentHoveredLinkIndex) {
            setLinkState(previousActiveLink, Mudlet::HyperlinkStyling::StateHover);
        } else if (isLinkVisited(previousActiveLink)) {  // Check base state: visited or default
            setLinkState(previousActiveLink, Mudlet::HyperlinkStyling::StateVisited);
        } else {  // Otherwise return to default
            setLinkState(previousActiveLink, Mudlet::HyperlinkStyling::StateDefault);
        }

        updateLinkCharacters(previousActiveLink); // Update displayed characters
    }

    // Set new link to active state
    if (linkIndex > 0) {
        setLinkState(linkIndex, Mudlet::HyperlinkStyling::StateActive);
        updateLinkCharacters(linkIndex); // Update displayed characters
    }
}

void TBuffer::setFocusedLink(int linkIndex)
{
    int previousFocusedLink = mCurrentFocusedLinkIndex;
    mCurrentFocusedLinkIndex = linkIndex;

    // Reset previous focused link
    if (previousFocusedLink > 0 && previousFocusedLink != linkIndex) {
        setLinkState(previousFocusedLink, Mudlet::HyperlinkStyling::StateDefault);
    }

    // Set new link to focus state
    if (linkIndex > 0) {
        // Determine if focus came from keyboard (would use StateFocusVisible)
        // For now, we use StateFocus
        setLinkState(linkIndex, Mudlet::HyperlinkStyling::StateFocus);
    }
}

void TBuffer::markLinkAsVisited(int linkIndex)
{
    if (linkIndex > 0) {
        mVisitedLinks[linkIndex] = true;

        // If the link is not in an interactive state (hover/active/focus),
        // update it to visited state immediately
        auto currentState = getLinkState(linkIndex);

        if (currentState == Mudlet::HyperlinkStyling::StateDefault) {
            setLinkState(linkIndex, Mudlet::HyperlinkStyling::StateVisited);
            updateLinkCharacters(linkIndex);
        }

#if defined(DEBUG_OSC_PROCESSING)
        qDebug() << "[OSC8] Link" << linkIndex << "marked as visited";
#endif
    }
}

bool TBuffer::isLinkVisited(int linkIndex) const
{
    return mVisitedLinks.value(linkIndex, false);
}

int TBuffer::getLinkIndexAt(int line, int column) const
{
    // Validate line bounds
    if (line < 0 || line >= lineBuffer.size()) {
        return 0;
    }

    const auto& bufferLine = buffer.at(line);

    // Validate column bounds
    if (column < 0 || column >= static_cast<int>(bufferLine.size())) {
        return 0;
    }

    // Return the link index at this position
    return bufferLine.at(column).linkIndex();
}

// Update all TChar objects in the buffer that have the specified linkIndex
// with the effective styling for the current state
void TBuffer::updateLinkCharacters(int linkIndex)
{
    if (linkIndex <= 0) {
        return;
    }

    // Get the effective styling for this link's current state
    Mudlet::HyperlinkStyling effectiveStyling = getEffectiveHyperlinkStyling(linkIndex);

    // IMPORTANT: If this link has no custom CSS styling at all (neither base nor pseudo-class),
    // don't modify the characters. This preserves ANSI formatting for links without any CSS.
    if (!effectiveStyling.hasCustomStyling) {
#if defined(DEBUG_OSC_PROCESSING)
        qDebug() << "[OSC8] Link" << linkIndex << "has no custom styling - preserving ANSI formatting";
#endif
        return; // Don't modify characters - preserve original ANSI formatting
    }

    // Check if we should use ANSI base with pseudo-class overlays:
    // - Has pseudo-class styling (hasCustomStyling = true)
    // - But NO base CSS styling (hasBaseCustomStyling = false)
    // - And we're in default/visited state (not hover/active/focus)
    bool useAnsiBase = !effectiveStyling.hasBaseCustomStyling &&
                       (effectiveStyling.currentState == Mudlet::HyperlinkStyling::StateDefault ||
                        effectiveStyling.currentState == Mudlet::HyperlinkStyling::StateVisited);

#if defined(DEBUG_OSC_PROCESSING)
    qDebug() << "[OSC8] Updating link" << linkIndex << "- hasBaseCustomStyling:" << effectiveStyling.hasBaseCustomStyling
             << "currentState:" << effectiveStyling.currentState << "useAnsiBase:" << useAnsiBase;
#endif

    // Iterate through all lines in the buffer
    for (auto& line : buffer) {
        // Iterate through all characters in the line
        for (auto& tchar : line) {
            // Check if this character belongs to the link we're updating
            if (tchar.linkIndex() == linkIndex) {
                // Apply the effective styling to this character

                // If we should use ANSI base (no base CSS but in default/visited state),
                // restore the original ANSI colors and formatting as a BASE,
                // then let pseudo-class CSS styling override it
                if (useAnsiBase && mLinkOriginalCharacters.contains(linkIndex)) {
                    TChar originalChar = mLinkOriginalCharacters.value(linkIndex);
#if defined(DEBUG_OSC_PROCESSING)
                    qDebug() << "[OSC8] Restoring ANSI base for link" << linkIndex
                             << "- Original FgColor:" << originalChar.mFgColor.name()
                             << "Original BgColor:" << originalChar.mBgColor.name()
                             << "Original Bold:" << bool(originalChar.mFlags & TChar::Bold)
                             << "Current FgColor:" << tchar.mFgColor.name()
                             << "Current Bold:" << bool(tchar.mFlags & TChar::Bold);
#endif
                    // Restore ANSI base - these will be overridden below if CSS specifies them
                    tchar.mFgColor = originalChar.mFgColor;
                    tchar.mBgColor = originalChar.mBgColor;
                    tchar.mFlags = originalChar.mFlags; // Restore ALL ANSI formatting flags including decorations

                    // Clear any custom decoration colors that were applied by CSS pseudo-classes
                    // ANSI doesn't support custom decoration colors, so we clear them when restoring ANSI base
                    tchar.clearCustomUnderlineColor();
                    tchar.clearCustomOverlineColor();
                    tchar.clearCustomStrikeoutColor();

                    // DON'T continue here - let the CSS pseudo-class styling below override the ANSI base
                    // This allows e.g. :visited{color:#bb66dd} to work with ANSI base formatting
                }

                // Apply CSS styling

                // Update foreground color
                if (effectiveStyling.hasForegroundColor) {
                    tchar.mFgColor = effectiveStyling.foregroundColor;
                }

                // Update background color - restore original if not specified in styling
                if (effectiveStyling.hasBackgroundColor) {
                    tchar.mBgColor = effectiveStyling.backgroundColor;
                } else {
                    // Restore the original background color from when the link was created
                    tchar.mBgColor = mLinkOriginalBackgrounds.value(linkIndex, mBackGroundColor);
                }

                // Update text decorations (only for CSS styling, not ANSI-base)
                if (effectiveStyling.isUnderlined) {
                    tchar.mFlags |= TChar::Underline;

                    // Apply underline style
                    // First clear any existing underline style flags
                    tchar.mFlags &= ~(TChar::UnderlineWavy | TChar::UnderlineDotted | TChar::UnderlineDashed);

                    switch (effectiveStyling.underlineStyle) {
                        case Mudlet::HyperlinkStyling::UnderlineWavy:
                            tchar.mFlags |= TChar::UnderlineWavy;
                            break;
                        case Mudlet::HyperlinkStyling::UnderlineDotted:
                            tchar.mFlags |= TChar::UnderlineDotted;
                            break;
                        case Mudlet::HyperlinkStyling::UnderlineDashed:
                            tchar.mFlags |= TChar::UnderlineDashed;
                            break;
                        default:
                            break;
                    }
                } else {
                    tchar.mFlags &= ~TChar::Underline;
                    tchar.mFlags &= ~(TChar::UnderlineWavy | TChar::UnderlineDotted | TChar::UnderlineDashed);
                }

                if (effectiveStyling.isOverlined) {
                    tchar.mFlags |= TChar::Overline;
                } else {
                    tchar.mFlags &= ~TChar::Overline;
                }

                if (effectiveStyling.isStrikeOut) {
                    tchar.mFlags |= TChar::StrikeOut;
                } else {
                    tchar.mFlags &= ~TChar::StrikeOut;
                }

                // Apply decoration colors
                if (effectiveStyling.hasUnderlineColor && effectiveStyling.isUnderlined) {
                    tchar.setUnderlineColor(effectiveStyling.underlineColor);
                }
                if (effectiveStyling.hasOverlineColor && effectiveStyling.isOverlined) {
                    tchar.setOverlineColor(effectiveStyling.overlineColor);
                }
                if (effectiveStyling.hasStrikeoutColor && effectiveStyling.isStrikeOut) {
                    tchar.setStrikeoutColor(effectiveStyling.strikeoutColor);
                }

                // Update bold and italic
                if (effectiveStyling.isBold) {
                    tchar.mFlags |= TChar::Bold;
                } else {
                    tchar.mFlags &= ~TChar::Bold;
                }

                if (effectiveStyling.isItalic) {
                    tchar.mFlags |= TChar::Italic;
                } else {
                    tchar.mFlags &= ~TChar::Italic;
                }
            }
        }
    }
}
